diff --git a/Dockerfile b/Dockerfile
index 7d26a63b0..061e06de0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,10 +9,16 @@ WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/dynamic-apps/package.json packages/dynamic-apps/package.json
+COPY packages/dynamic-apps-core/package.json packages/dynamic-apps-core/package.json
COPY packages/dynamic-apps-builder/package.json packages/dynamic-apps-builder/package.json
COPY benchmarks/dynamic-apps/package.json benchmarks/dynamic-apps/package.json
COPY examples/apps-ai-builder/package.json examples/apps-ai-builder/package.json
COPY examples/apps-hello-world/package.json examples/apps-hello-world/package.json
+COPY examples/apps-core-quickstart/package.json examples/apps-core-quickstart/package.json
+COPY examples/apps-multiplayer/package.json examples/apps-multiplayer/package.json
+COPY examples/apps-sqlite/package.json examples/apps-sqlite/package.json
+COPY examples/apps-static-website/package.json examples/apps-static-website/package.json
+COPY examples/apps-workflows/package.json examples/apps-workflows/package.json
COPY tests/e2e/dynamic-apps/package.json tests/e2e/dynamic-apps/package.json
RUN pnpm install --frozen-lockfile
diff --git a/benchmarks/dynamic-apps/package.json b/benchmarks/dynamic-apps/package.json
index a9689247c..9179f275f 100644
--- a/benchmarks/dynamic-apps/package.json
+++ b/benchmarks/dynamic-apps/package.json
@@ -4,20 +4,20 @@
"private": true,
"type": "module",
"scripts": {
- "benchmark:local": "node --import tsx src/local.ts --host 0.0.0.0",
- "benchmark:cloud-stress": "node --import tsx src/cloud-stress.ts",
- "benchmark:stress": "node --expose-gc --import tsx src/runtime-stress.ts",
- "benchmark:suite": "node --import tsx src/suite.ts",
- "deploy:fixture": "node --import tsx src/deploy-fixture.ts",
- "load": "node --import tsx src/load.ts",
- "start": "node --import tsx src/server.ts --host 0.0.0.0",
+ "benchmark:local": "npx tsx src/local.ts --host 0.0.0.0",
+ "benchmark:cloud-stress": "npx tsx src/cloud-stress.ts",
+ "benchmark:stress": "npx tsx --expose-gc src/runtime-stress.ts",
+ "benchmark:suite": "npx tsx src/suite.ts",
+ "deploy:fixture": "npx tsx src/deploy-fixture.ts",
+ "load": "npx tsx src/load.ts",
+ "start": "npx tsx src/server.ts --host 0.0.0.0",
"check-types": "tsc --noEmit",
- "test": "node --import tsx --test src/load.test.ts"
+ "test": "npx tsx --test src/load.test.ts"
},
"dependencies": {
"@hono/node-server": "^2.0.11",
- "@rivet-dev/agentos-core": "0.2.15",
- "@rivet-dev/agentos-toolchain": "0.2.15",
+ "@rivet-dev/agentos-core": "0.2.16-rc.3",
+ "@rivet-dev/agentos-toolchain": "0.2.16-rc.3",
"@rivet-dev/dynamic-apps": "workspace:*",
"@rivet-dev/dynamic-apps-core": "workspace:*",
"hono": "^4.12.9",
diff --git a/benchmarks/dynamic-apps/src/deploy-fixture.ts b/benchmarks/dynamic-apps/src/deploy-fixture.ts
index 05bcc4be4..7bc18731d 100644
--- a/benchmarks/dynamic-apps/src/deploy-fixture.ts
+++ b/benchmarks/dynamic-apps/src/deploy-fixture.ts
@@ -6,7 +6,7 @@ import {
} from "./fixture.js";
const actorClient = createClient() as unknown as {
- agentOSAppsApp: {
+ dynamicAppsApp: {
getOrCreate(key: string[]): ActorHandle;
getForId(actorId: string): ActorHandle;
};
@@ -16,10 +16,10 @@ interface ActorHandle {
}
const actorId = process.env.BENCH_APP_ACTOR_ID;
const actor = actorId
- ? actorClient.agentOSAppsApp.getForId(actorId)
- : actorClient.agentOSAppsApp.getOrCreate([BENCHMARK_APP_ID]);
+ ? actorClient.dynamicAppsApp.getForId(actorId)
+ : actorClient.dynamicAppsApp.getOrCreate([BENCHMARK_APP_ID]);
const deploymentClient = {
- agentOSAppsApp: { getOrCreate: () => actor },
+ dynamicAppsApp: { getOrCreate: () => actor },
} as unknown as BenchmarkDeploymentClient;
const deployment = await deployBenchmarkFixture(deploymentClient);
const resolvedActorId = await actor.resolve();
diff --git a/benchmarks/dynamic-apps/src/edge.ts b/benchmarks/dynamic-apps/src/edge.ts
index cfe8888f6..b79aea369 100644
--- a/benchmarks/dynamic-apps/src/edge.ts
+++ b/benchmarks/dynamic-apps/src/edge.ts
@@ -22,7 +22,7 @@ interface AppHandle {
}
interface StateClient {
- agentOSAppsApp: {
+ dynamicAppsApp: {
getOrCreate(key: string[]): AppHandle;
};
}
@@ -84,7 +84,7 @@ export function createBenchmarkApplication(): Hono {
| undefined;
let actorApplicationClient: ActorApplicationClient | undefined;
const getActorApplicationDeployment = () => {
- actorApplicationDeployment ??= deployActorBenchmarkFixture(client)
+ actorApplicationDeployment ??= deployActorBenchmarkFixture()
.then((deployment) => {
actorApplicationResolvedDeployment = deployment;
return deployment;
@@ -101,12 +101,7 @@ export function createBenchmarkApplication(): Hono {
) as unknown as ActorApplicationClient;
return actorApplicationClient;
};
- const privateRegistry = (request: Request) => {
- const headers = new Headers(request.headers);
- 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) => appsRouter.fetch(c.req.raw));
app.all("/bench/noop", () => {
const startedAt = performance.now();
@@ -148,7 +143,7 @@ export function createBenchmarkApplication(): Hono {
app.all("/bench/actor/resolve", async () => {
const startedAt = performance.now();
- const handle = client.agentOSAppsApp.getOrCreate([BENCHMARK_APP_ID]);
+ const handle = client.dynamicAppsApp.getOrCreate([BENCHMARK_APP_ID]);
const actorId = await handle.resolve();
const response = Response.json({ ok: true, actorId });
response.headers.set(
@@ -159,7 +154,7 @@ export function createBenchmarkApplication(): Hono {
});
app.all("/bench/actor/action", async () => {
const startedAt = performance.now();
- const resolution = await client.agentOSAppsApp
+ const resolution = await client.dynamicAppsApp
.getOrCreate([BENCHMARK_APP_ID])
.resolveDeployment();
const response = Response.json({ ok: true, resolution });
@@ -245,7 +240,7 @@ export function createBenchmarkApplication(): Hono {
});
app.get("/bench/setup", async () => {
try {
- const deployment = await client.agentOSAppsApp
+ const deployment = await client.dynamicAppsApp
.getOrCreate([BENCHMARK_APP_ID])
.resolveDeployment();
return Response.json({ status: "ready", deployment });
@@ -254,7 +249,7 @@ export function createBenchmarkApplication(): Hono {
typeof error === "object" && error !== null && "code" in error
? String(error.code)
: undefined;
- if (code === "agentos_apps_not_deployed") {
+ if (code === "dynamic_apps_not_deployed") {
return Response.json({ status: "not-started" }, { status: 404 });
}
throw error;
diff --git a/benchmarks/dynamic-apps/src/load.ts b/benchmarks/dynamic-apps/src/load.ts
index d2a0c9eab..e8abd3292 100644
--- a/benchmarks/dynamic-apps/src/load.ts
+++ b/benchmarks/dynamic-apps/src/load.ts
@@ -147,7 +147,7 @@ export async function runLoadTest(
{
signal: AbortSignal.timeout(config.timeoutMs),
headers: {
- "user-agent": "agentos-apps-load-test",
+ "user-agent": "dynamic-apps-load-test",
...(config.token ? { "x-rivet-token": config.token } : {}),
},
},
diff --git a/benchmarks/dynamic-apps/src/runtime-stress.ts b/benchmarks/dynamic-apps/src/runtime-stress.ts
index 157dc610a..05c691d3d 100644
--- a/benchmarks/dynamic-apps/src/runtime-stress.ts
+++ b/benchmarks/dynamic-apps/src/runtime-stress.ts
@@ -107,7 +107,7 @@ class FakeStatePlane {
}
readonly client = {
- agentOSAppsApp: {
+ dynamicAppsApp: {
getOrCreate: (key: string[]) => this.#handle(key[0] ?? ""),
},
};
@@ -641,7 +641,7 @@ async function admissionStress(
.then(
() => "ok" as const,
(error: unknown) =>
- isErrorCode(error, "agentos_apps_no_capacity")
+ isErrorCode(error, "dynamic_apps_no_capacity")
? ("rejected" as const)
: Promise.reject(error),
),
@@ -841,7 +841,7 @@ async function actorChurnStress(
await response.arrayBuffer();
completed += 1;
} catch (error) {
- if (!isErrorCode(error, "agentos_apps_no_capacity")) throw error;
+ if (!isErrorCode(error, "dynamic_apps_no_capacity")) throw error;
rejected += 1;
}
});
@@ -1204,7 +1204,7 @@ async function actorMemoryStress(
assert.equal(Number(await response.text()), allocationBytes);
completed += 1;
} catch (error) {
- if (!isErrorCode(error, "agentos_apps_no_capacity")) throw error;
+ if (!isErrorCode(error, "dynamic_apps_no_capacity")) throw error;
rejected += 1;
}
});
diff --git a/docs/content/docs/authentication.mdx b/docs/content/docs/authentication.mdx
index ad0799f02..02fc6e54e 100644
--- a/docs/content/docs/authentication.mdx
+++ b/docs/content/docs/authentication.mdx
@@ -3,8 +3,6 @@ title: "Authentication"
description: "Authenticate requests to deployed Dynamic Apps with Hono middleware, route guards, and application-level authorization before handlers run."
---
-## Authentication
-
Use normal Hono middleware to authenticate requests before they reach deployed
apps:
diff --git a/docs/content/docs/deploy.mdx b/docs/content/docs/deploy.mdx
index 30905a1a1..041ea6ee8 100644
--- a/docs/content/docs/deploy.mdx
+++ b/docs/content/docs/deploy.mdx
@@ -38,8 +38,9 @@ await deployApp({
The direct entrypoint must default-export a function or an object with
`fetch(request)`. Code runs inside agentOS with filesystem, process,
environment, and network permissions, and supported Node builtins are
-available. Static-only directories and native addons are not supported in this
-release candidate.
+available. Directories that contain only static files are rejected; see
+[Static Websites](/dynamic-apps/docs/static-websites). Native addons are not
+supported.
## Build repair and rollback
@@ -58,6 +59,10 @@ for (let attempt = 0; attempt < 3; attempt++) {
}
```
+Include `webServerSkill` and `rivetActorsSkill` from `@rivet-dev/dynamic-apps`
+in the model prompt. They describe the supported TypeScript server layout and
+Rivet actor integration. [View the complete AI App Builder example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-ai-builder).
+
A failed build or incomplete artifact write never replaces the active release.
A successful call returns only after the immutable artifact is persisted and
activated; it does not mean a request-serving replica was warmed.
@@ -88,7 +93,7 @@ await deployApp({
| Option | Default | Meaning |
| --- | --- | --- |
| `regions` | State actor's current region | Stored compatibility metadata; it does not move direct HTTP execution |
-| `createNamespace` | — | Deprecated compatibility option; every app always receives its own stable namespace |
+| `createNamespace` | none | Deprecated compatibility option; every app always receives its own stable namespace |
| `scaling.minReplicas` | `0` | App-defined actor runner setting; compatibility metadata for direct HTTP |
| `scaling.maxReplicas` | `128` | App-defined actor runner setting; compatibility metadata for direct HTTP |
| `scaling.targetConcurrency` | `8` | App-defined actor runner setting; compatibility metadata for direct HTTP |
diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx
index 3bfe0a0cb..872b4bbfc 100644
--- a/docs/content/docs/index.mdx
+++ b/docs/content/docs/index.mdx
@@ -1,45 +1,89 @@
---
title: "Dynamic Apps"
-description: "Build user-generated HTTP apps and serve them through bounded agentOS VMs."
+description: "Deploy user-generated applications in isolated agentOS VMs with SQLite, workflows, multiplayer, and static sites out of the box."
skill: true
---
-Dynamic Apps builds user-generated HTTP applications in a sandboxed deployment
-VM and serves ordinary HTTP through agentOS in your own server process. The
-default `@rivet-dev/dynamic-apps` package stores releases in a per-app Rivet
-actor; `@rivet-dev/dynamic-apps-core` lets you supply another store.
+Dynamic Apps runs user-generated HTTP applications inside your own Node.js
+server. Each app runs in an isolated agentOS VM and can add durable SQLite
+state, workflows, realtime multiplayer, and static sites.
Dynamic Apps is in preview and its API is subject to change.
+
+
+ Start the host server, deploy a generated app, and visit it.
+
+
+ Deploy files or a directory, repair build errors, and configure apps.
+
+
+ Serve an HTML, CSS, and JavaScript site.
+
+
+ Store durable relational data in an actor-owned database.
+
+
+ Share realtime state between every connected client.
+
+
+ Run durable multi-step jobs that sleep and resume.
+
+
+
## Architecture
-**Dynamic Apps is a library, not a hosted app deployment platform.** You own the
-Hono server, its authentication, and the URL on which applications are mounted.
+**Dynamic Apps is a library, not a hosted app deployment platform.** You own
+the Hono server, its authentication, and the URL on which applications are
+mounted.
+
+Requests reach your Hono server, where `appsRouter` executes them in a cached
+agentOS VM holding the app's active release. Warm requests never touch storage
+or a Rivet actor.
+
+
-`appsRouter` executes each request in a clean JavaScript context. The default
-mode keeps a small bounded pool of retained agentOS contexts, resetting and
-reinitializing each one after use. Ephemeral mode asks agentOS for a fresh
-context on every request while reusing the immutable release VM.
+Deployment is separate from serving. `deployApp()` builds the files in a
+sandboxed agentOS build VM and publishes an immutable release. The default
+`@rivet-dev/dynamic-apps` package stores releases in a per-app Rivet actor;
+`@rivet-dev/dynamic-apps-core` lets you supply another store.
-An application may additionally export a RivetKit registry. Those app-defined
-actors use normal Rivet routing for durable state, actions, events, and
-connections, while the same application's ordinary HTTP handler still runs
-through headless agentOS evaluation.
+An app may also export a RivetKit registry. Those app-defined actors use normal
+Rivet routing for durable state, actions, events, and connections, while the
+same app's ordinary HTTP handler still runs through the agentOS VM.
agentOS provides the filesystem, process, environment, and network permission
diff --git a/docs/content/docs/multiplayer.mdx b/docs/content/docs/multiplayer.mdx
new file mode 100644
index 000000000..65a134353
--- /dev/null
+++ b/docs/content/docs/multiplayer.mdx
@@ -0,0 +1,19 @@
+---
+title: "Multiplayer"
+description: "Share realtime state between every client connected to an app."
+---
+
+An actor holds the shared state for one room and broadcasts events to every
+connected client. [View the complete example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-multiplayer).
+
+The server is the generated app. The client shows how your own system connects
+to the actors inside it.
+
+
+
+
+
+
+Dynamic Apps does not wrap RivetKit's action, event, or connection APIs. See
+[Events](https://rivet.dev/actors/docs/events/) and
+[Connections](https://rivet.dev/actors/docs/connections/) in Rivet Actors.
diff --git a/docs/content/docs/quickstart-core.mdx b/docs/content/docs/quickstart-core.mdx
index c6e670dfc..ab14bdfa0 100644
--- a/docs/content/docs/quickstart-core.mdx
+++ b/docs/content/docs/quickstart-core.mdx
@@ -1,5 +1,5 @@
---
-title: "Core Quick Start"
+title: "Core Quickstart"
description: "Build and serve a Dynamic App with a development-only in-memory release store."
skill: true
---
@@ -59,7 +59,7 @@ disposes the instance during shutdown.
Pass the listening host on the command line:
```sh
-node --import tsx src/server.ts --host 0.0.0.0
+npx tsx src/server.ts --host 0.0.0.0
```
diff --git a/docs/content/docs/quickstart.mdx b/docs/content/docs/quickstart.mdx
index 2e113f419..9b3e92714 100644
--- a/docs/content/docs/quickstart.mdx
+++ b/docs/content/docs/quickstart.mdx
@@ -33,7 +33,7 @@ served by `appsRouter` through a cached agentOS VM.
Run the server normally:
```sh
-node --import tsx src/server.ts
+npx tsx src/server.ts
```
@@ -46,7 +46,7 @@ agent, an upload endpoint, or another trusted control-plane process.
```sh
-node --import tsx src/deploy.ts
+npx tsx src/deploy.ts
```
diff --git a/docs/content/docs/realtime.mdx b/docs/content/docs/realtime.mdx
deleted file mode 100644
index 6c34f1cb8..000000000
--- a/docs/content/docs/realtime.mdx
+++ /dev/null
@@ -1,22 +0,0 @@
----
-title: "Realtime Events"
-description: "Subscribe to events from an app-defined RivetKit actor."
----
-
-App-defined actors expose the standard RivetKit realtime client. Subscribe to
-an event on a connected actor handle:
-
-```ts
-const counter = await client.counter.getOrCreate(["main"]).connect();
-
-const unsubscribe = counter.on("changed", (count) => {
- console.log("count changed", count);
-});
-
-await counter.add(1);
-unsubscribe();
-await counter.dispose();
-```
-
-Dynamic Apps packages the actor registry and configures its stable runner pool;
-it does not wrap or replace RivetKit's action, event, or connection APIs.
diff --git a/docs/content/docs/reference.mdx b/docs/content/docs/reference.mdx
deleted file mode 100644
index 75cd82ec8..000000000
--- a/docs/content/docs/reference.mdx
+++ /dev/null
@@ -1,19 +0,0 @@
----
-title: "Reference"
-description: "Build and repair Dynamic Apps with AI-generated files, deployment diagnostics, and a practical roadmap for extending generated backends."
----
-
-## Build Apps with AI
-
-Give an agent the app requirements, let it generate the project files, and pass
-those files to `deployApp()`. If the build returns TypeScript diagnostics, give
-them back to the agent and deploy its repaired files again.
-
-[View the complete AI App Builder example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-ai-builder).
-
-## Planned Improvements
-
-- Automatically include agent skills based on the
- [Actors Learn guides](https://rivet.dev/actors/learn/) for better app generation.
-- Billing API for tracking and charging for app usage.
-- Built-in error reporting for generated apps.
diff --git a/docs/content/docs/routing.mdx b/docs/content/docs/routing.mdx
index ecb70598e..6bebd4cf2 100644
--- a/docs/content/docs/routing.mdx
+++ b/docs/content/docs/routing.mdx
@@ -5,8 +5,7 @@ skill: true
---
The package root exports one router. Mount ordinary application traffic at any
-prefix and explicitly dispatch the private `/api/rivet` callback to the same
-router:
+prefix and forward the private `/api/rivet/*` callback to the same router:
```ts
import { appsRouter } from "@rivet-dev/dynamic-apps";
@@ -14,14 +13,7 @@ import { Hono } from "hono";
const server = new Hono();
-const dispatchRegistry = (request: Request) => {
- const headers = new Headers(request.headers);
- headers.set("x-agentos-app-registry-dispatch", "1");
- return appsRouter.fetch(new Request(request, { headers }));
-};
-
-server.all("/api/rivet", (c) => dispatchRegistry(c.req.raw));
-server.all("/api/rivet/*", (c) => dispatchRegistry(c.req.raw));
+server.all("/api/rivet/*", (c) => appsRouter.fetch(c.req.raw));
server.route("/apps", appsRouter);
```
@@ -30,7 +22,3 @@ This serves `/:appId/*` relative to the mount. For example,
`/api/items?q=1`. A bare `/apps/example` request redirects to
`/apps/example/`.
-There is no `createAppsRouter` or router-specific client option in the default
-adapter. `appsRouter` creates and reuses its private control client lazily. Once
-an active artifact is prepared, warm requests use only the local runtime and
-isolate caches: they do not call release storage or a Rivet actor.
diff --git a/docs/content/docs/sqlite.mdx b/docs/content/docs/sqlite.mdx
new file mode 100644
index 000000000..00b201659
--- /dev/null
+++ b/docs/content/docs/sqlite.mdx
@@ -0,0 +1,20 @@
+---
+title: "SQLite"
+description: "Store durable relational data in an actor-owned SQLite database."
+---
+
+Apps that depend on `rivetkit` can define actors. Each actor owns its own
+SQLite database, so a generated app gets durable relational data without any
+extra infrastructure. [View the complete example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-sqlite).
+
+The server is the generated app. The client shows how your own system connects
+to the actors inside it.
+
+
+
+
+
+
+`deployApp()` returns the endpoint, namespace, pool, and token the ordinary
+RivetKit client needs. See [SQLite in Rivet Actors](https://rivet.dev/actors/docs/sqlite/)
+for the full database API.
diff --git a/docs/content/docs/state-and-data.mdx b/docs/content/docs/state-and-data.mdx
deleted file mode 100644
index 589e1bc8d..000000000
--- a/docs/content/docs/state-and-data.mdx
+++ /dev/null
@@ -1,49 +0,0 @@
----
-title: "State & Actors"
-description: "Add durable RivetKit actors while ordinary HTTP runs through agentOS."
----
-
-An application can combine ordinary HTTP with app-defined RivetKit actors.
-Declare `rivetkit` in the application's dependencies, export its registry, and
-keep the normal `registry.start()` call:
-
-```ts
-import { actor, event, setup } from "rivetkit";
-
-const counter = actor({
- state: { count: 0 },
- events: { changed: event() },
- actions: {
- add(c, amount = 1) {
- c.state.count += amount;
- c.broadcast("changed", c.state.count);
- return c.state.count;
- },
- },
-});
-
-export const registry = setup({ use: { counter } });
-registry.start();
-
-export default () => new Response("ordinary HTTP still runs locally");
-```
-
-`deployApp()` returns the stable namespace and runner-pool names needed by the
-ordinary RivetKit client:
-
-```ts
-import { createClient } from "rivetkit/client";
-
-const deployment = await deployApp({ appId: "counter", source });
-const client = createClient({
- namespace: deployment.namespace,
- poolName: deployment.pool,
-});
-
-const handle = await client.counter.getOrCreate(["main"]).connect();
-console.log(await handle.add(1));
-```
-
-Actor actions, state, events, connections, and streaming actor responses use
-normal Rivet routing. Requests to the application's default HTTP export still
-use the cached agentOS direct-request path.
diff --git a/docs/content/docs/static-websites.mdx b/docs/content/docs/static-websites.mdx
new file mode 100644
index 000000000..f265d6136
--- /dev/null
+++ b/docs/content/docs/static-websites.mdx
@@ -0,0 +1,21 @@
+---
+title: "Static Websites"
+description: "Serve an HTML, CSS, and JavaScript site from a Dynamic App."
+---
+
+An app is a directory with a `package.json` and a server entrypoint. A static
+site is the same directory plus a `public/` folder and a handler that serves
+it. [View the complete example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-static-website).
+
+
+
+
+
+
+
+Deploy the directory and open `/apps/static-website/`:
+
+
+
+Directories that contain only static files are rejected. Every app needs a
+`package.json` and an entrypoint that default-exports a `fetch` handler.
diff --git a/docs/content/docs/workflows.mdx b/docs/content/docs/workflows.mdx
new file mode 100644
index 000000000..9f595ac71
--- /dev/null
+++ b/docs/content/docs/workflows.mdx
@@ -0,0 +1,19 @@
+---
+title: "Workflows"
+description: "Run durable multi-step jobs that sleep, scale to zero, and resume."
+---
+
+An actor's `run` workflow executes when the actor is created. Each step is
+durable, so the job survives restarts and the app scales to zero while it
+sleeps. [View the complete example](https://github.com/rivet-dev/dynamic-apps/tree/main/examples/apps-workflows).
+
+The server is the generated app. The client shows how your own system connects
+to the actors inside it.
+
+
+
+
+
+
+See [Workflows in Rivet Actors](https://rivet.dev/actors/docs/workflows/) for
+steps, loops, queues, and error handling.
diff --git a/docs/sidebar.json b/docs/sidebar.json
index ec0074867..2616afda2 100644
--- a/docs/sidebar.json
+++ b/docs/sidebar.json
@@ -9,12 +9,12 @@
"icon": "faSquareInfo"
},
{
- "title": "Quick Start",
+ "title": "Quickstart",
"href": "/dynamic-apps/docs/quickstart",
"icon": "faForwardFast"
},
{
- "title": "Core Quick Start",
+ "title": "Core Quickstart",
"href": "/dynamic-apps/docs/quickstart-core",
"icon": "faForwardFast"
}
@@ -41,16 +41,20 @@
"title": "Capabilities",
"pages": [
{
- "title": "State & Actors",
- "href": "/dynamic-apps/docs/state-and-data"
+ "title": "Static Websites",
+ "href": "/dynamic-apps/docs/static-websites"
},
{
- "title": "Realtime Events",
- "href": "/dynamic-apps/docs/realtime"
+ "title": "SQLite",
+ "href": "/dynamic-apps/docs/sqlite"
},
{
- "title": "Collecting logs",
- "href": "/dynamic-apps/docs/logging"
+ "title": "Multiplayer",
+ "href": "/dynamic-apps/docs/multiplayer"
+ },
+ {
+ "title": "Workflows",
+ "href": "/dynamic-apps/docs/workflows"
}
]
},
@@ -62,8 +66,8 @@
"href": "/dynamic-apps/docs/authentication"
},
{
- "title": "Reference",
- "href": "/dynamic-apps/docs/reference"
+ "title": "Collecting logs",
+ "href": "/dynamic-apps/docs/logging"
}
]
}
diff --git a/examples/apps-ai-builder/fixtures/app/package.json b/examples/apps-ai-builder/fixtures/app/package.json
index f866b0c16..7174e192b 100644
--- a/examples/apps-ai-builder/fixtures/app/package.json
+++ b/examples/apps-ai-builder/fixtures/app/package.json
@@ -8,7 +8,12 @@
"build": "tsc",
"check-types": "tsc --noEmit"
},
+ "dependencies": {
+ "@hono/node-server": "2.0.11",
+ "hono": "4.12.9"
+ },
"devDependencies": {
+ "@types/node": "22.19.15",
"typescript": "5.7.3"
}
}
diff --git a/examples/apps-ai-builder/fixtures/app/src/index.ts b/examples/apps-ai-builder/fixtures/app/src/index.ts
index c369cd297..88dc11adf 100644
--- a/examples/apps-ai-builder/fixtures/app/src/index.ts
+++ b/examples/apps-ai-builder/fixtures/app/src/index.ts
@@ -1,8 +1,15 @@
-export default {
- fetch(request: Request) {
- return Response.json({
- message: "Replace this seed with the generated application.",
- path: new URL(request.url).pathname,
- });
- },
-};
+import { serve } from "@hono/node-server";
+import { Hono } from "hono";
+
+const app = new Hono();
+
+app.get("/", (context) =>
+ context.json({
+ message: "Replace this seed with the generated application.",
+ }),
+);
+
+serve({
+ fetch: app.fetch,
+ port: Number(process.env.PORT ?? 3000),
+});
diff --git a/examples/apps-ai-builder/package.json b/examples/apps-ai-builder/package.json
index cc05f174d..cab81a21e 100644
--- a/examples/apps-ai-builder/package.json
+++ b/examples/apps-ai-builder/package.json
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
- "start": "node --import tsx src/server.ts",
+ "start": "npx tsx src/server.ts",
"check-types": "tsc --noEmit"
},
"dependencies": {
diff --git a/examples/apps-ai-builder/src/server.ts b/examples/apps-ai-builder/src/server.ts
index 509a0a684..fd08a9800 100644
--- a/examples/apps-ai-builder/src/server.ts
+++ b/examples/apps-ai-builder/src/server.ts
@@ -1,7 +1,12 @@
import { readFile } from "node:fs/promises";
import { anthropic } from "@ai-sdk/anthropic";
import { serve } from "@hono/node-server";
-import { appsRouter, deployApp } from "@rivet-dev/dynamic-apps";
+import {
+ appsRouter,
+ deployApp,
+ rivetActorsSkill,
+ webServerSkill,
+} from "@rivet-dev/dynamic-apps";
import { generateText } from "ai";
import { Hono } from "hono";
@@ -53,9 +58,11 @@ async function revise(
model: anthropic(process.env.AI_MODEL ?? "claude-sonnet-4-5"),
maxOutputTokens: 8_000,
prompt: [
+ // These skills tell the model how to structure, build, and serve the generated code.
+ webServerSkill,
+ rivetActorsSkill,
'Return JSON only as {"files":{"path":"content"}}.',
`You may edit only: ${editablePaths.join(", ")}.`,
- "The app must export a default object with fetch(request) returning a Web Response.",
`User request: ${prompt}`,
diagnostics ? `Previous build diagnostics:\n${diagnostics}` : "",
`Current files:\n${JSON.stringify(files)}`,
@@ -80,7 +87,7 @@ async function generateApp(appId: string, prompt: string) {
error !== null &&
"code" in error &&
typeof error.code === "string" &&
- error.code.startsWith("agentos_apps_");
+ error.code.startsWith("dynamic_apps_");
if (!appsError || attempt === maxRepairs) {
throw error;
}
@@ -101,13 +108,7 @@ async function generateApp(appId: string, prompt: string) {
}
const server = new Hono();
-const dispatchRegistry = (request: Request) => {
- const headers = new Headers(request.headers);
- headers.set("x-agentos-app-registry-dispatch", "1");
- return appsRouter.fetch(new Request(request, { headers }));
-};
-server.all("/api/rivet", (c) => dispatchRegistry(c.req.raw));
-server.all("/api/rivet/*", (c) => dispatchRegistry(c.req.raw));
+server.all("/api/rivet/*", (c) => appsRouter.fetch(c.req.raw));
// An agent or any other part of the system can call this route. A generic
// deployment endpoint could accept multipart files; this example generates the
diff --git a/examples/apps-core-quickstart/package.json b/examples/apps-core-quickstart/package.json
index 59f5a3667..e0ae2bb2d 100644
--- a/examples/apps-core-quickstart/package.json
+++ b/examples/apps-core-quickstart/package.json
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
- "start": "node --import tsx src/server.ts --host 0.0.0.0",
+ "start": "npx tsx src/server.ts --host 0.0.0.0",
"check-types": "tsc --noEmit"
},
"dependencies": {
diff --git a/examples/apps-hello-world/package.json b/examples/apps-hello-world/package.json
index 70e05afa7..993787fd1 100644
--- a/examples/apps-hello-world/package.json
+++ b/examples/apps-hello-world/package.json
@@ -4,8 +4,8 @@
"private": true,
"type": "module",
"scripts": {
- "start": "node --import tsx src/server.ts",
- "deploy": "node --import tsx src/deploy.ts",
+ "start": "npx tsx src/server.ts",
+ "deploy": "npx tsx src/deploy.ts",
"check-types": "tsc --noEmit"
},
"dependencies": {
diff --git a/examples/apps-hello-world/src/server.ts b/examples/apps-hello-world/src/server.ts
index 811fdead4..b13cf5934 100644
--- a/examples/apps-hello-world/src/server.ts
+++ b/examples/apps-hello-world/src/server.ts
@@ -4,13 +4,7 @@ import { Hono } from "hono";
const server = new Hono();
-const dispatchRegistry = (request: Request) => {
- const headers = new Headers(request.headers);
- headers.set("x-agentos-app-registry-dispatch", "1");
- return appsRouter.fetch(new Request(request, { headers }));
-};
-server.all("/api/rivet", (c) => dispatchRegistry(c.req.raw));
-server.all("/api/rivet/*", (c) => dispatchRegistry(c.req.raw));
+server.all("/api/rivet/*", (c) => appsRouter.fetch(c.req.raw));
// Mount every deployed application at /apps/:appId.
server.route("/apps", appsRouter);
diff --git a/examples/apps-multiplayer/fixtures/app/package.json b/examples/apps-multiplayer/fixtures/app/package.json
new file mode 100644
index 000000000..7da6914f1
--- /dev/null
+++ b/examples/apps-multiplayer/fixtures/app/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "multiplayer-room-app",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "main": "src/index.ts",
+ "dependencies": {
+ "@hono/node-server": "2.1.1",
+ "hono": "4.13.3",
+ "rivetkit": "2.3.11"
+ }
+}
diff --git a/examples/apps-multiplayer/fixtures/app/src/index.ts b/examples/apps-multiplayer/fixtures/app/src/index.ts
new file mode 100644
index 000000000..9be651b42
--- /dev/null
+++ b/examples/apps-multiplayer/fixtures/app/src/index.ts
@@ -0,0 +1,44 @@
+import { serve } from "@hono/node-server";
+import { Hono } from "hono";
+import { actor, event, setup } from "rivetkit";
+
+type Position = { x: number; y: number };
+
+// One actor per room. State is shared by every connected client.
+const room = actor({
+ state: { players: {} as Record },
+ events: { changed: event() },
+ actions: {
+ join(c, player: string) {
+ c.state.players[player] ??= { x: 0, y: 0 };
+ c.broadcast("changed", c.state.players);
+ return c.state.players;
+ },
+ move(c, player: string, x: number, y: number) {
+ c.state.players[player] = { x, y };
+ c.broadcast("changed", c.state.players);
+ return c.state.players;
+ },
+ },
+});
+
+export const registry = setup({ use: { room } });
+
+const app = new Hono();
+app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
+app.get("/", (c) =>
+ c.json({ message: "Use the RivetKit client to join a room." }),
+);
+
+// Dynamic Apps runs the actors in serverless mode and waits for this listener.
+if (process.env.RIVETKIT_RUNTIME_MODE === "serverless") {
+ await new Promise((resolve, reject) => {
+ const server = serve(
+ { fetch: app.fetch, port: Number(process.env.PORT), hostname: "0.0.0.0" },
+ () => resolve(),
+ );
+ server.once("error", reject);
+ });
+}
+
+export default app;
diff --git a/examples/apps-multiplayer/package.json b/examples/apps-multiplayer/package.json
new file mode 100644
index 000000000..dad4ae8f8
--- /dev/null
+++ b/examples/apps-multiplayer/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@rivet-dev/dynamic-apps-example-multiplayer",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "start": "npx tsx src/server.ts",
+ "client": "npx tsx src/client.ts",
+ "check-types": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@hono/node-server": "^2.0.11",
+ "@rivet-dev/dynamic-apps": "workspace:*",
+ "hono": "^4.12.9",
+ "rivetkit": "2.3.11"
+ },
+ "devDependencies": {
+ "@types/node": "^22.19.15",
+ "tsx": "^4.20.6",
+ "typescript": "^5.7.3"
+ }
+}
diff --git a/examples/apps-multiplayer/src/client.ts b/examples/apps-multiplayer/src/client.ts
new file mode 100644
index 000000000..a8abb7473
--- /dev/null
+++ b/examples/apps-multiplayer/src/client.ts
@@ -0,0 +1,32 @@
+import type { deployApp } from "@rivet-dev/dynamic-apps";
+import { createClient } from "rivetkit/client";
+import type { registry } from "../fixtures/app/src/index.js";
+
+// Deploy the app through the host server and read back its connection details.
+const host = process.env.HOST_URL ?? "http://localhost:3000";
+const response = await fetch(`${host}/deploy/multiplayer-room`, {
+ method: "POST",
+});
+if (!response.ok) throw new Error(`deploy failed: ${await response.text()}`);
+const deployment = (await response.json()) as Awaited<
+ ReturnType
+>;
+
+// docs:start client
+const client = createClient({
+ endpoint: deployment.endpoint,
+ namespace: deployment.namespace,
+ poolName: deployment.pool,
+ token: deployment.token,
+});
+
+// Open a realtime connection and receive every broadcast from the room.
+const room = client.room.getOrCreate(["lobby"]).connect();
+room.on("changed", (players) => console.log("players", players));
+
+await room.join("alice");
+await room.move("alice", 4, 8);
+await room.dispose();
+// docs:end client
+
+await client.dispose();
diff --git a/examples/apps-multiplayer/src/server.ts b/examples/apps-multiplayer/src/server.ts
new file mode 100644
index 000000000..32e26acca
--- /dev/null
+++ b/examples/apps-multiplayer/src/server.ts
@@ -0,0 +1,21 @@
+import { serve } from "@hono/node-server";
+import { appsRouter, deployApp } from "@rivet-dev/dynamic-apps";
+import { Hono } from "hono";
+
+const server = new Hono();
+server.all("/api/rivet/*", (c) => appsRouter.fetch(c.req.raw));
+
+// An agent or upload endpoint would pass generated files here. This example
+// deploys its checked-in fixture and returns the deployment to the caller.
+server.post("/deploy/:name", async (c) =>
+ c.json(
+ await deployApp({
+ appId: c.req.param("name"),
+ source: new URL("../fixtures/app/", import.meta.url),
+ }),
+ ),
+);
+
+server.route("/apps", appsRouter);
+
+serve({ fetch: server.fetch, port: Number(process.env.PORT ?? 3000) });
diff --git a/examples/apps-multiplayer/tsconfig.json b/examples/apps-multiplayer/tsconfig.json
new file mode 100644
index 000000000..888653f31
--- /dev/null
+++ b/examples/apps-multiplayer/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022", "DOM"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": true
+ },
+ "include": ["src", "fixtures/app/src"]
+}
diff --git a/examples/apps-sqlite/fixtures/app/package.json b/examples/apps-sqlite/fixtures/app/package.json
new file mode 100644
index 000000000..52522ca5a
--- /dev/null
+++ b/examples/apps-sqlite/fixtures/app/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "sqlite-notes-app",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "main": "src/index.ts",
+ "dependencies": {
+ "@hono/node-server": "2.1.1",
+ "hono": "4.13.3",
+ "rivetkit": "2.3.11"
+ }
+}
diff --git a/examples/apps-sqlite/fixtures/app/src/index.ts b/examples/apps-sqlite/fixtures/app/src/index.ts
new file mode 100644
index 000000000..553a9efe7
--- /dev/null
+++ b/examples/apps-sqlite/fixtures/app/src/index.ts
@@ -0,0 +1,47 @@
+import { serve } from "@hono/node-server";
+import { Hono } from "hono";
+import { actor, setup } from "rivetkit";
+import { db } from "rivetkit/db";
+
+// Each actor owns its own SQLite database.
+const notes = actor({
+ db: db({
+ async onMigrate(database) {
+ await database.execute(`
+ CREATE TABLE IF NOT EXISTS notes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ body TEXT NOT NULL
+ )
+ `);
+ },
+ }),
+ actions: {
+ async add(c, body: string) {
+ await c.db.execute("INSERT INTO notes (body) VALUES (?)", body);
+ },
+ async list(c) {
+ return c.db.execute("SELECT id, body FROM notes ORDER BY id");
+ },
+ },
+});
+
+export const registry = setup({ use: { notes } });
+
+const app = new Hono();
+app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
+app.get("/", (c) =>
+ c.json({ message: "Use the RivetKit client to add notes." }),
+);
+
+// Dynamic Apps runs the actors in serverless mode and waits for this listener.
+if (process.env.RIVETKIT_RUNTIME_MODE === "serverless") {
+ await new Promise((resolve, reject) => {
+ const server = serve(
+ { fetch: app.fetch, port: Number(process.env.PORT), hostname: "0.0.0.0" },
+ () => resolve(),
+ );
+ server.once("error", reject);
+ });
+}
+
+export default app;
diff --git a/examples/apps-sqlite/package.json b/examples/apps-sqlite/package.json
new file mode 100644
index 000000000..84bdb1dbe
--- /dev/null
+++ b/examples/apps-sqlite/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@rivet-dev/dynamic-apps-example-sqlite",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "start": "npx tsx src/server.ts",
+ "client": "npx tsx src/client.ts",
+ "check-types": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@hono/node-server": "^2.0.11",
+ "@rivet-dev/dynamic-apps": "workspace:*",
+ "hono": "^4.12.9",
+ "rivetkit": "2.3.11"
+ },
+ "devDependencies": {
+ "@types/node": "^22.19.15",
+ "tsx": "^4.20.6",
+ "typescript": "^5.7.3"
+ }
+}
diff --git a/examples/apps-sqlite/src/client.ts b/examples/apps-sqlite/src/client.ts
new file mode 100644
index 000000000..245c65114
--- /dev/null
+++ b/examples/apps-sqlite/src/client.ts
@@ -0,0 +1,29 @@
+import type { deployApp } from "@rivet-dev/dynamic-apps";
+import { createClient } from "rivetkit/client";
+import type { registry } from "../fixtures/app/src/index.js";
+
+// Deploy the app through the host server and read back its connection details.
+const host = process.env.HOST_URL ?? "http://localhost:3000";
+const response = await fetch(`${host}/deploy/sqlite-notes`, {
+ method: "POST",
+});
+if (!response.ok) throw new Error(`deploy failed: ${await response.text()}`);
+const deployment = (await response.json()) as Awaited<
+ ReturnType
+>;
+
+// docs:start client
+// Connect to the actors inside the app's own Rivet namespace.
+const client = createClient({
+ endpoint: deployment.endpoint,
+ namespace: deployment.namespace,
+ poolName: deployment.pool,
+ token: deployment.token,
+});
+
+const notes = client.notes.getOrCreate(["shared"]);
+await notes.add("Hello from the RivetKit client");
+console.log(await notes.list());
+// docs:end client
+
+await client.dispose();
diff --git a/examples/apps-sqlite/src/server.ts b/examples/apps-sqlite/src/server.ts
new file mode 100644
index 000000000..32e26acca
--- /dev/null
+++ b/examples/apps-sqlite/src/server.ts
@@ -0,0 +1,21 @@
+import { serve } from "@hono/node-server";
+import { appsRouter, deployApp } from "@rivet-dev/dynamic-apps";
+import { Hono } from "hono";
+
+const server = new Hono();
+server.all("/api/rivet/*", (c) => appsRouter.fetch(c.req.raw));
+
+// An agent or upload endpoint would pass generated files here. This example
+// deploys its checked-in fixture and returns the deployment to the caller.
+server.post("/deploy/:name", async (c) =>
+ c.json(
+ await deployApp({
+ appId: c.req.param("name"),
+ source: new URL("../fixtures/app/", import.meta.url),
+ }),
+ ),
+);
+
+server.route("/apps", appsRouter);
+
+serve({ fetch: server.fetch, port: Number(process.env.PORT ?? 3000) });
diff --git a/examples/apps-sqlite/tsconfig.json b/examples/apps-sqlite/tsconfig.json
new file mode 100644
index 000000000..888653f31
--- /dev/null
+++ b/examples/apps-sqlite/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022", "DOM"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": true
+ },
+ "include": ["src", "fixtures/app/src"]
+}
diff --git a/examples/apps-static-website/fixtures/app/package.json b/examples/apps-static-website/fixtures/app/package.json
new file mode 100644
index 000000000..2119d84bf
--- /dev/null
+++ b/examples/apps-static-website/fixtures/app/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "static-website-app",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "main": "src/index.ts",
+ "dependencies": {
+ "hono": "4.13.3"
+ }
+}
diff --git a/examples/apps-static-website/fixtures/app/public/app.js b/examples/apps-static-website/fixtures/app/public/app.js
new file mode 100644
index 000000000..f304d933a
--- /dev/null
+++ b/examples/apps-static-website/fixtures/app/public/app.js
@@ -0,0 +1,2 @@
+document.querySelector("#status").textContent =
+ "Served from a Dynamic Apps release inside agentOS.";
diff --git a/examples/apps-static-website/fixtures/app/public/index.html b/examples/apps-static-website/fixtures/app/public/index.html
new file mode 100644
index 000000000..4c9fae5c6
--- /dev/null
+++ b/examples/apps-static-website/fixtures/app/public/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Static site on Dynamic Apps
+
+
+
+
+ Static sites scale to zero too.
+ Loading JavaScript…
+
+
+
+
diff --git a/examples/apps-static-website/fixtures/app/public/styles.css b/examples/apps-static-website/fixtures/app/public/styles.css
new file mode 100644
index 000000000..0bc9f4b40
--- /dev/null
+++ b/examples/apps-static-website/fixtures/app/public/styles.css
@@ -0,0 +1,5 @@
+body {
+ font-family: system-ui, sans-serif;
+ margin: 4rem auto;
+ max-width: 40rem;
+}
diff --git a/examples/apps-static-website/fixtures/app/src/index.ts b/examples/apps-static-website/fixtures/app/src/index.ts
new file mode 100644
index 000000000..f56526ae6
--- /dev/null
+++ b/examples/apps-static-website/fixtures/app/src/index.ts
@@ -0,0 +1,28 @@
+import { readFile } from "node:fs/promises";
+import { Hono } from "hono";
+
+const contentTypes: Record = {
+ ".html": "text/html; charset=utf-8",
+ ".css": "text/css; charset=utf-8",
+ ".js": "text/javascript; charset=utf-8",
+};
+
+const app = new Hono();
+
+// Serve every file under public/. "/" maps to public/index.html. The build
+// bundles this entrypoint next to public/, so paths resolve from the bundle.
+app.get("/*", async (c) => {
+ const path = c.req.path.endsWith("/")
+ ? `${c.req.path}index.html`
+ : c.req.path;
+ const type = contentTypes[path.slice(path.lastIndexOf("."))];
+ if (!type || path.includes("..")) return c.notFound();
+ try {
+ const file = await readFile(new URL(`./public${path}`, import.meta.url));
+ return c.body(file, 200, { "content-type": type });
+ } catch {
+ return c.notFound();
+ }
+});
+
+export default app;
diff --git a/examples/apps-static-website/package.json b/examples/apps-static-website/package.json
new file mode 100644
index 000000000..3e6e165b2
--- /dev/null
+++ b/examples/apps-static-website/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@rivet-dev/dynamic-apps-example-static-website",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "start": "npx tsx src/server.ts",
+ "check-types": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@hono/node-server": "^2.0.11",
+ "@rivet-dev/dynamic-apps": "workspace:*",
+ "hono": "^4.12.9"
+ },
+ "devDependencies": {
+ "@types/node": "^22.19.15",
+ "tsx": "^4.20.6",
+ "typescript": "^5.7.3"
+ }
+}
diff --git a/examples/apps-static-website/src/server.ts b/examples/apps-static-website/src/server.ts
new file mode 100644
index 000000000..d08f4a02a
--- /dev/null
+++ b/examples/apps-static-website/src/server.ts
@@ -0,0 +1,18 @@
+import { serve } from "@hono/node-server";
+import { appsRouter, deployApp } from "@rivet-dev/dynamic-apps";
+import { Hono } from "hono";
+
+const server = new Hono();
+server.all("/api/rivet/*", (c) => appsRouter.fetch(c.req.raw));
+server.route("/apps", appsRouter);
+
+// docs:start deploy
+// Deploy the site directory. Its package.json and src/index.ts serve public/.
+await deployApp({
+ appId: "static-website",
+ source: new URL("../fixtures/app/", import.meta.url),
+});
+// docs:end deploy
+
+serve({ fetch: server.fetch, port: Number(process.env.PORT ?? 3000) });
+console.log("Open http://localhost:3000/apps/static-website/");
diff --git a/examples/apps-static-website/tsconfig.json b/examples/apps-static-website/tsconfig.json
new file mode 100644
index 000000000..888653f31
--- /dev/null
+++ b/examples/apps-static-website/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022", "DOM"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": true
+ },
+ "include": ["src", "fixtures/app/src"]
+}
diff --git a/examples/apps-workflows/fixtures/app/package.json b/examples/apps-workflows/fixtures/app/package.json
new file mode 100644
index 000000000..fd1f12ca2
--- /dev/null
+++ b/examples/apps-workflows/fixtures/app/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "order-workflow-app",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "main": "src/index.ts",
+ "dependencies": {
+ "@hono/node-server": "2.1.1",
+ "hono": "4.13.3",
+ "rivetkit": "2.3.11"
+ }
+}
diff --git a/examples/apps-workflows/fixtures/app/src/index.ts b/examples/apps-workflows/fixtures/app/src/index.ts
new file mode 100644
index 000000000..3b5439e93
--- /dev/null
+++ b/examples/apps-workflows/fixtures/app/src/index.ts
@@ -0,0 +1,48 @@
+import { serve } from "@hono/node-server";
+import { Hono } from "hono";
+import { actor, setup } from "rivetkit";
+import { workflow } from "rivetkit/workflow";
+
+type Status = "placed" | "paid" | "shipped" | "delivered";
+
+// The workflow runs when the actor is created. Each step is durable, so the
+// actor can sleep, scale to zero, and resume exactly where it left off.
+const order = actor({
+ state: { status: "placed" as Status },
+ actions: {
+ status: (c) => c.state.status,
+ },
+ run: workflow(async (wf) => {
+ await wf.step("charge", async (c) => {
+ c.state.status = "paid";
+ });
+ await wf.step("ship", async (c) => {
+ c.state.status = "shipped";
+ });
+ await wf.sleep("in transit", 2_000);
+ await wf.step("deliver", async (c) => {
+ c.state.status = "delivered";
+ });
+ }),
+});
+
+export const registry = setup({ use: { order } });
+
+const app = new Hono();
+app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
+app.get("/", (c) =>
+ c.json({ message: "Use the RivetKit client to read orders." }),
+);
+
+// Dynamic Apps runs the actors in serverless mode and waits for this listener.
+if (process.env.RIVETKIT_RUNTIME_MODE === "serverless") {
+ await new Promise((resolve, reject) => {
+ const server = serve(
+ { fetch: app.fetch, port: Number(process.env.PORT), hostname: "0.0.0.0" },
+ () => resolve(),
+ );
+ server.once("error", reject);
+ });
+}
+
+export default app;
diff --git a/examples/apps-workflows/package.json b/examples/apps-workflows/package.json
new file mode 100644
index 000000000..aa1d61911
--- /dev/null
+++ b/examples/apps-workflows/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@rivet-dev/dynamic-apps-example-workflows",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "start": "npx tsx src/server.ts",
+ "client": "npx tsx src/client.ts",
+ "check-types": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@hono/node-server": "^2.0.11",
+ "@rivet-dev/dynamic-apps": "workspace:*",
+ "hono": "^4.12.9",
+ "rivetkit": "2.3.11"
+ },
+ "devDependencies": {
+ "@types/node": "^22.19.15",
+ "tsx": "^4.20.6",
+ "typescript": "^5.7.3"
+ }
+}
diff --git a/examples/apps-workflows/src/client.ts b/examples/apps-workflows/src/client.ts
new file mode 100644
index 000000000..192041117
--- /dev/null
+++ b/examples/apps-workflows/src/client.ts
@@ -0,0 +1,34 @@
+import type { deployApp } from "@rivet-dev/dynamic-apps";
+import { createClient } from "rivetkit/client";
+import type { registry } from "../fixtures/app/src/index.js";
+
+// Deploy the app through the host server and read back its connection details.
+const host = process.env.HOST_URL ?? "http://localhost:3000";
+const response = await fetch(`${host}/deploy/order-workflow`, {
+ method: "POST",
+});
+if (!response.ok) throw new Error(`deploy failed: ${await response.text()}`);
+const deployment = (await response.json()) as Awaited<
+ ReturnType
+>;
+
+// docs:start client
+const client = createClient({
+ endpoint: deployment.endpoint,
+ namespace: deployment.namespace,
+ poolName: deployment.pool,
+ token: deployment.token,
+});
+
+// Creating the actor starts its workflow. Poll until it finishes.
+const order = client.order.getOrCreate(["order-1042"]);
+let status = await order.status();
+while (status !== "delivered") {
+ console.log("status", status);
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ status = await order.status();
+}
+console.log("status", status);
+// docs:end client
+
+await client.dispose();
diff --git a/examples/apps-workflows/src/server.ts b/examples/apps-workflows/src/server.ts
new file mode 100644
index 000000000..32e26acca
--- /dev/null
+++ b/examples/apps-workflows/src/server.ts
@@ -0,0 +1,21 @@
+import { serve } from "@hono/node-server";
+import { appsRouter, deployApp } from "@rivet-dev/dynamic-apps";
+import { Hono } from "hono";
+
+const server = new Hono();
+server.all("/api/rivet/*", (c) => appsRouter.fetch(c.req.raw));
+
+// An agent or upload endpoint would pass generated files here. This example
+// deploys its checked-in fixture and returns the deployment to the caller.
+server.post("/deploy/:name", async (c) =>
+ c.json(
+ await deployApp({
+ appId: c.req.param("name"),
+ source: new URL("../fixtures/app/", import.meta.url),
+ }),
+ ),
+);
+
+server.route("/apps", appsRouter);
+
+serve({ fetch: server.fetch, port: Number(process.env.PORT ?? 3000) });
diff --git a/examples/apps-workflows/tsconfig.json b/examples/apps-workflows/tsconfig.json
new file mode 100644
index 000000000..888653f31
--- /dev/null
+++ b/examples/apps-workflows/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022", "DOM"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "noEmit": true,
+ "skipLibCheck": true
+ },
+ "include": ["src", "fixtures/app/src"]
+}
diff --git a/packages/dynamic-apps-builder/cli/apps-builder.mjs b/packages/dynamic-apps-builder/cli/apps-builder.mjs
index bc56d19fc..406fc782e 100755
--- a/packages/dynamic-apps-builder/cli/apps-builder.mjs
+++ b/packages/dynamic-apps-builder/cli/apps-builder.mjs
@@ -340,8 +340,10 @@ function nodeFileSystemPlugin() {
if (
config.usesRivetKit &&
(extension === ".js" ||
+ extension === ".cjs" ||
extension === ".mjs" ||
- extension === ".ts")
+ extension === ".ts" ||
+ extension === ".cts")
) {
// RivetKit deliberately hides this optional dependency from
// ordinary bundlers. Apps always select the WASM runtime, so
@@ -349,10 +351,19 @@ function nodeFileSystemPlugin() {
// leaving the native and engine-CLI fallbacks unreachable.
const source = contents.toString("utf8");
contents = Buffer.from(
- source.replaceAll(
- 'import(["@rivetkit", "rivetkit-wasm"].join("/"))',
- 'import("@rivetkit/rivetkit-wasm")',
- ),
+ source
+ .replace(
+ /import\(\s*\[\s*["']@rivetkit["']\s*,\s*["']rivetkit-wasm["']\s*\]\.join\(\s*["']\/["']\s*\)\s*\)/gu,
+ 'import("@rivetkit/rivetkit-wasm")',
+ )
+ .replace(
+ /require\(\s*\[\s*["']@rivetkit["']\s*,\s*["']rivetkit-wasm["']\s*\]\.join\(\s*["']\/["']\s*\)\s*\)/gu,
+ 'require("@rivetkit/rivetkit-wasm")',
+ )
+ .replace(
+ "const { getEnginePath } = await loadEngineCli();",
+ 'if (!config.startEngine) throw new Error("local Engine disabled"); const { getEnginePath } = await loadEngineCli();',
+ ),
);
}
return {
diff --git a/packages/dynamic-apps-builder/package.json b/packages/dynamic-apps-builder/package.json
index f8c4e6525..c1b207183 100644
--- a/packages/dynamic-apps-builder/package.json
+++ b/packages/dynamic-apps-builder/package.json
@@ -34,12 +34,13 @@
"test": "vitest run test/ --passWithNoTests"
},
"dependencies": {
+ "@rivetkit/rivetkit-wasm": "2.3.11",
"esbuild-wasm": "0.27.4",
"rivetkit": "2.3.11"
},
"devDependencies": {
- "@rivet-dev/agentos-core": "0.2.15",
- "@rivet-dev/agentos-toolchain": "0.2.15",
+ "@rivet-dev/agentos-core": "0.2.16-rc.3",
+ "@rivet-dev/agentos-toolchain": "0.2.16-rc.3",
"@types/node": "^22.19.15",
"typescript": "^5.7.3",
"vitest": "^2.1.9"
diff --git a/packages/dynamic-apps-builder/test/builder.test.ts b/packages/dynamic-apps-builder/test/builder.test.ts
index f472f601c..1efc76504 100644
--- a/packages/dynamic-apps-builder/test/builder.test.ts
+++ b/packages/dynamic-apps-builder/test/builder.test.ts
@@ -1,6 +1,7 @@
-import { execFile } from "node:child_process";
+import { execFile, spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
+import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
@@ -19,7 +20,7 @@ const builder = join(packageRoot, "cli", "apps-builder.mjs");
describe("apps-builder", () => {
test("emits an executable Node ESM dispatcher with Node builtins", async () => {
- const root = await mkdtemp(join(tmpdir(), "agentos-apps-direct-builder-"));
+ const root = await mkdtemp(join(tmpdir(), "dynamic-apps-direct-builder-"));
const workspace = join(root, "workspace");
const release = join(root, "release");
await mkdir(workspace, { recursive: true });
@@ -74,7 +75,7 @@ describe("apps-builder", () => {
});
test("bundles real RivetKit and invokes it inside agentOS", async () => {
- const root = await mkdtemp(join(tmpdir(), "agentos-apps-rivetkit-"));
+ const root = await mkdtemp(join(tmpdir(), "dynamic-apps-rivetkit-"));
const workspace = join(root, "workspace");
const release = join(root, "release");
await mkdir(workspace, { recursive: true });
@@ -140,7 +141,7 @@ describe("apps-builder", () => {
}, 30_000);
test("rejects native Node addons in direct agentOS bundles", async () => {
- const root = await mkdtemp(join(tmpdir(), "agentos-apps-native-addon-"));
+ const root = await mkdtemp(join(tmpdir(), "dynamic-apps-native-addon-"));
const workspace = join(root, "workspace");
const release = join(root, "release");
await mkdir(workspace, { recursive: true });
@@ -173,7 +174,7 @@ describe("apps-builder", () => {
});
test("emits a minimal executable TypeScript release with static assets", async () => {
- const root = await mkdtemp(join(tmpdir(), "agentos-apps-builder-"));
+ const root = await mkdtemp(join(tmpdir(), "dynamic-apps-builder-"));
const workspace = join(root, "workspace");
const release = join(root, "release");
await mkdir(join(workspace, "src"), { recursive: true });
@@ -282,8 +283,8 @@ describe("apps-builder", () => {
});
});
- test("emits a small platform-linked actor fetch bundle", async () => {
- const root = await mkdtemp(join(tmpdir(), "agentos-apps-actor-builder-"));
+ test("emits a self-contained AgentOS RivetKit WASM actor bundle", async () => {
+ const root = await mkdtemp(join(tmpdir(), "dynamic-apps-actor-builder-"));
const workspace = join(root, "workspace");
const release = join(root, "release");
await mkdir(join(workspace, "src"), { recursive: true });
@@ -294,10 +295,15 @@ describe("apps-builder", () => {
await writeFile(
join(workspace, "src", "index.mjs"),
[
+ 'import http from "node:http";',
'import { actor, setup } from "rivetkit";',
"const counter = actor({ state: { count: 0 } });",
"const registry = setup({ use: { counter } });",
- "export default { fetch: (request) => registry.handler(request) };",
+ "http.createServer(async (incoming, outgoing) => {",
+ " const chunks = []; for await (const chunk of incoming) chunks.push(Buffer.from(chunk));",
+ " const response = await registry.handler(new Request(new URL(incoming.url ?? '/', 'http://actor.test'), { method: incoming.method, headers: incoming.headers, body: incoming.method === 'GET' || incoming.method === 'HEAD' ? undefined : Buffer.concat(chunks) }));",
+ " outgoing.statusCode = response.status; response.headers.forEach((value, name) => outgoing.setHeader(name, value)); outgoing.end(Buffer.from(await response.arrayBuffer()));",
+ "}).listen(Number(process.env.PORT), '0.0.0.0');",
].join("\n"),
);
const configPath = join(root, "config.json");
@@ -310,26 +316,87 @@ describe("apps-builder", () => {
version: "actor-test",
sourceFiles: ["src/index.mjs"],
usesRivetKit: true,
- platformRivetKit: true,
- maxOutputBytes: 1024 * 1024,
+ directAgentOs: true,
+ maxOutputBytes: 16 * 1024 * 1024,
maxOutputFiles: 16,
- maxFileBytes: 1024 * 1024,
+ maxFileBytes: 8 * 1024 * 1024,
}),
);
await execFileAsync(process.execPath, [builder, configPath]);
const paths = await listFiles(release);
- expect(paths).toEqual([
- "agentos-package.json",
- "main.mjs",
- "manifest.json",
- ]);
+ expect(paths).toEqual(
+ expect.arrayContaining([
+ "agentos-package.json",
+ "main.mjs",
+ "manifest.json",
+ ]),
+ );
+ expect(
+ paths.some(
+ (path) =>
+ path.startsWith("modules/rivetkit-") && path.endsWith(".wasm"),
+ ),
+ ).toBe(true);
const source = await readFile(join(release, "main.mjs"), "utf8");
- expect(source).toContain('from"rivetkit"');
- expect(Buffer.byteLength(source)).toBeLessThan(32 * 1024);
- });
+ expect(source).not.toContain("DYNAMIC_APPS_ACTOR_RPC");
+ expect(source).not.toContain("/.agentos/ready");
+ expect(source).not.toContain('from"rivetkit"');
+ expect(Buffer.byteLength(source)).toBeGreaterThan(256 * 1024);
+ const port = await freePort();
+ const child = spawn(process.execPath, [join(release, "main.mjs")], {
+ stdio: ["pipe", "pipe", "pipe"],
+ env: {
+ ...process.env,
+ PORT: String(port),
+ RIVETKIT_RUNTIME: "wasm",
+ RIVETKIT_RUNTIME_MODE: "serverless",
+ },
+ });
+ let stderr = "";
+ child.stdout.resume();
+ child.stderr.on("data", (chunk) => {
+ stderr += chunk;
+ });
+ await new Promise((resolveReady, rejectReady) => {
+ const timeout = setTimeout(() => {
+ child.kill("SIGKILL");
+ rejectReady(new Error(`actor bundle did not start: ${stderr}`));
+ }, 5_000);
+ const poll = setInterval(() => {
+ void fetch(`http://127.0.0.1:${port}/.agentos/ready`)
+ .then(() => {
+ clearInterval(poll);
+ clearTimeout(timeout);
+ resolveReady();
+ })
+ .catch(() => {});
+ }, 20);
+ child.once("exit", (code) => {
+ clearInterval(poll);
+ clearTimeout(timeout);
+ rejectReady(new Error(`actor bundle exited ${code}: ${stderr}`));
+ });
+ });
+ child.kill("SIGKILL");
+ }, 15_000);
});
+async function freePort(): Promise {
+ const server = createServer();
+ await new Promise((resolve, reject) => {
+ server.once("error", reject);
+ server.listen(0, "127.0.0.1", resolve);
+ });
+ const address = server.address();
+ if (!address || typeof address === "string")
+ throw new Error("failed to allocate port");
+ await new Promise((resolve, reject) =>
+ server.close((error) => (error ? reject(error) : resolve())),
+ );
+ return address.port;
+}
+
async function listFiles(root: string): Promise {
const paths: string[] = [];
const walk = async (directory: string) => {
diff --git a/packages/dynamic-apps-core/package.json b/packages/dynamic-apps-core/package.json
index 38d425a7e..8559c4256 100644
--- a/packages/dynamic-apps-core/package.json
+++ b/packages/dynamic-apps-core/package.json
@@ -32,10 +32,10 @@
"test": "vitest run"
},
"dependencies": {
- "@agentos-software/sh": "0.2.15",
+ "@agentos-software/sh": "0.2.16-rc.3",
"@agentos-software/tar": "0.3.5",
- "@rivet-dev/agentos-core": "0.2.15",
- "@rivet-dev/agentos-toolchain": "0.2.15",
+ "@rivet-dev/agentos-core": "0.2.16-rc.3",
+ "@rivet-dev/agentos-toolchain": "0.2.16-rc.3",
"@rivet-dev/dynamic-apps-builder": "workspace:0.12.0-rc.2",
"hono": "^4.7.0"
},
diff --git a/packages/dynamic-apps-core/src/artifact.ts b/packages/dynamic-apps-core/src/artifact.ts
index f09d8ac74..5c18750be 100644
--- a/packages/dynamic-apps-core/src/artifact.ts
+++ b/packages/dynamic-apps-core/src/artifact.ts
@@ -11,7 +11,7 @@ export function extractAospkgTextFile(
buffer.subarray(1, 4).toString("ascii") !== "AOS"
) {
throw new DynamicAppsError(
- "agentos_apps_artifact_format_invalid",
+ "dynamic_apps_artifact_format_invalid",
"application artifact is not an AOSP package",
);
}
@@ -36,7 +36,7 @@ export function extractAospkgTextFile(
offset = next;
}
throw new DynamicAppsError(
- "agentos_apps_artifact_entry_missing",
+ "dynamic_apps_artifact_entry_missing",
`application artifact is missing ${target}`,
);
}
diff --git a/packages/dynamic-apps-core/src/build.ts b/packages/dynamic-apps-core/src/build.ts
index f24d7f4dd..9ec74a6ce 100644
--- a/packages/dynamic-apps-core/src/build.ts
+++ b/packages/dynamic-apps-core/src/build.ts
@@ -109,7 +109,7 @@ export function readBuildConfig(
const maximum = DEFAULT_BUILD_CONFIG[key];
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
throw new DynamicAppsError(
- "agentos_apps_invalid_config",
+ "dynamic_apps_invalid_config",
`${key} must be an integer between 1 and ${maximum}`,
{ name: key, maximum },
);
@@ -167,14 +167,14 @@ export function validateDeployment(
): BuildPlan {
if (!input || typeof input !== "object" || !input.files) {
fail(
- "agentos_apps_invalid_files",
+ "dynamic_apps_invalid_files",
"deployApp files must contain the complete application tree",
);
}
const files = Object.entries(input.files);
if (files.length === 0 || files.length > limits.maxFiles) {
fail(
- "agentos_apps_file_count_limit",
+ "dynamic_apps_file_count_limit",
`deployment must contain between 1 and ${limits.maxFiles} files`,
{ observed: files.length, limit: limits.maxFiles },
);
@@ -185,13 +185,13 @@ export function validateDeployment(
const normalizedPath = normalizeAppPath(path);
if (normalizedFiles[normalizedPath]) {
fail(
- "agentos_apps_duplicate_file_path",
+ "dynamic_apps_duplicate_file_path",
`multiple deployment paths normalize to ${normalizedPath}`,
);
}
if (!(content instanceof Uint8Array)) {
fail(
- "agentos_apps_invalid_file",
+ "dynamic_apps_invalid_file",
`deployment file ${path} must be a Uint8Array`,
);
}
@@ -200,7 +200,7 @@ export function validateDeployment(
}
if (sourceBytes > limits.maxSourceBytes) {
fail(
- "agentos_apps_source_limit",
+ "dynamic_apps_source_limit",
`deployment source is ${sourceBytes} bytes, exceeding maxSourceBytes ${limits.maxSourceBytes}`,
{ observed: sourceBytes, limit: limits.maxSourceBytes },
);
@@ -209,7 +209,7 @@ export function validateDeployment(
const packageJsonSource = textFile(normalizedFiles, "package.json");
if (!packageJsonSource) {
fail(
- "agentos_apps_entrypoint_not_found",
+ "dynamic_apps_entrypoint_not_found",
"direct applications must contain package.json and a server entrypoint",
);
}
@@ -224,7 +224,7 @@ export function validateDeployment(
packageJson = JSON.parse(packageJsonSource);
} catch (error) {
fail(
- "agentos_apps_invalid_package_json",
+ "dynamic_apps_invalid_package_json",
"package.json is not valid JSON",
{ error: String(error) },
);
@@ -242,7 +242,7 @@ export function validateDeployment(
);
if (dependencyCount > limits.maxDependencies) {
fail(
- "agentos_apps_dependency_limit",
+ "dynamic_apps_dependency_limit",
`deployment has ${dependencyCount} dependencies, exceeding maxDependencies ${limits.maxDependencies}`,
{ observed: dependencyCount, limit: limits.maxDependencies },
);
@@ -280,7 +280,7 @@ export function validateDeployment(
}
}
fail(
- "agentos_apps_entrypoint_not_found",
+ "dynamic_apps_entrypoint_not_found",
"could not infer a direct server entrypoint",
);
}
@@ -298,7 +298,7 @@ export function throwCommandFailure(
maxOutputBytes: number,
): never {
fail(
- `agentos_apps_${kind}_failed`,
+ `dynamic_apps_${kind}_failed`,
`${command} failed with exit code ${result.exitCode}`,
{
exitCode: result.exitCode,
@@ -316,7 +316,7 @@ function artifactResult(
): BuiltAppRelease {
if (!(bytesInput instanceof Uint8Array) || bytesInput.byteLength > maxBytes) {
fail(
- "agentos_apps_build_artifact_size_limit",
+ "dynamic_apps_build_artifact_size_limit",
`cached artifact exceeds ${maxBytes} bytes`,
);
}
@@ -433,7 +433,7 @@ async function buildRelease(
const failedWrite = writes.find((entry) => !entry.success);
if (failedWrite) {
fail(
- "agentos_apps_build_write_failed",
+ "dynamic_apps_build_write_failed",
`failed to write build input ${failedWrite.path}: ${failedWrite.error ?? "unknown error"}`,
{ path: failedWrite.path, error: failedWrite.error },
);
@@ -518,7 +518,7 @@ async function buildRelease(
);
if (nativeAddonCheck.exitCode === 42) {
fail(
- "agentos_apps_native_addon_unsupported",
+ "dynamic_apps_native_addon_unsupported",
"application contains native Node addons",
{
files: boundedOutput(
@@ -546,9 +546,8 @@ async function buildRelease(
release: "/release/direct",
entrypoint: "direct-runner.mjs",
sourceFiles: Object.keys(input.files),
- usesRivetKit: false,
- directIsolate: true,
- stubRivetKit: plan.usesRivetKit,
+ usesRivetKit: plan.usesRivetKit,
+ directAgentOs: true,
maxOutputBytes: config.maxBuildArtifactBytes,
maxOutputFiles: config.maxBuildArtifactFiles,
maxFileBytes: config.maxBuildArtifactFileBytes,
@@ -558,7 +557,7 @@ async function buildRelease(
const failedConfigWrite = configWrites.find((entry) => !entry.success);
if (failedConfigWrite) {
fail(
- "agentos_apps_build_write_failed",
+ "dynamic_apps_build_write_failed",
`failed to write Apps builder input ${failedConfigWrite.path}`,
);
}
@@ -591,8 +590,7 @@ async function buildRelease(
entrypoint: "actor-runner.mjs",
sourceFiles: Object.keys(input.files),
usesRivetKit: true,
- directIsolate: false,
- platformRivetKit: true,
+ directAgentOs: true,
maxOutputBytes: config.maxBuildArtifactBytes,
maxOutputFiles: config.maxBuildArtifactFiles,
maxFileBytes: config.maxBuildArtifactFileBytes,
@@ -601,7 +599,7 @@ async function buildRelease(
]);
if (actorConfigWrite.some((entry) => !entry.success)) {
fail(
- "agentos_apps_build_write_failed",
+ "dynamic_apps_build_write_failed",
"failed to write actor Apps builder input",
);
}
@@ -638,7 +636,7 @@ async function buildRelease(
);
if (validation.exitCode !== 0) {
fail(
- "agentos_apps_invalid_handler",
+ "dynamic_apps_invalid_handler",
"application entrypoint could not be imported as a direct fetch handler",
{
stderr: boundedOutput(validation.stderr, config.maxBuildOutputBytes),
@@ -653,7 +651,7 @@ async function buildRelease(
]);
if (rootManifestWrite.some((entry) => !entry.success)) {
fail(
- "agentos_apps_build_write_failed",
+ "dynamic_apps_build_write_failed",
"failed to write root application package manifest",
);
}
@@ -687,14 +685,14 @@ async function buildRelease(
archiveSize > config.maxBuildArtifactBytes
) {
fail(
- "agentos_apps_build_artifact_size_limit",
+ "dynamic_apps_build_artifact_size_limit",
`built application archive is ${archiveSize} bytes, limit is ${config.maxBuildArtifactBytes}`,
);
}
const sourceTar = Buffer.from(await build.readArtifact());
if (sourceTar.byteLength !== archiveSize) {
fail(
- "agentos_apps_build_artifact_truncated",
+ "dynamic_apps_build_artifact_truncated",
`build artifact contained ${sourceTar.byteLength} bytes, expected ${archiveSize}`,
);
}
@@ -741,7 +739,7 @@ export function createBuildVmFactory(
};
return async () => {
const outputDirectory = await mkdtemp(
- join(tmpdir(), "agentos-apps-build-output-"),
+ join(tmpdir(), "dynamic-apps-build-output-"),
);
await chmod(outputDirectory, 0o777);
const artifactGuestPath = "/agentos-app-output/agentos-app.tar";
diff --git a/packages/dynamic-apps-core/src/executor.ts b/packages/dynamic-apps-core/src/executor.ts
index 6046fba04..af92f20ea 100644
--- a/packages/dynamic-apps-core/src/executor.ts
+++ b/packages/dynamic-apps-core/src/executor.ts
@@ -156,7 +156,7 @@ export function readExecutorConfig(
const executionMode = env.DYNAMIC_APPS_EXECUTION_MODE ?? "pooled";
if (executionMode !== "ephemeral" && executionMode !== "pooled") {
throw new DynamicAppsError(
- "agentos_apps_invalid_config",
+ "dynamic_apps_invalid_config",
"DYNAMIC_APPS_EXECUTION_MODE must be ephemeral or pooled",
);
}
@@ -322,7 +322,7 @@ export function resolveExecutorConfig(
function invalidExecutorConfig(name: string): DynamicAppsError {
return new DynamicAppsError(
- "agentos_apps_invalid_config",
+ "dynamic_apps_invalid_config",
`invalid Dynamic Apps executor config ${name}`,
);
}
@@ -362,7 +362,7 @@ export class DynamicAppsExecutor {
): Promise {
if (this.#disposed) {
throw new DynamicAppsError(
- "agentos_apps_executor_disposed",
+ "dynamic_apps_executor_disposed",
"Dynamic Apps executor is shutting down",
);
}
@@ -396,7 +396,7 @@ export class DynamicAppsExecutor {
!mapping.resolution.regions.includes(requestedRegion)
) {
throw new DynamicAppsError(
- "agentos_apps_region_not_deployed",
+ "dynamic_apps_region_not_deployed",
`app is not deployed in requested region ${requestedRegion}`,
{ requestedRegion, regions: mapping.resolution.regions },
);
@@ -519,7 +519,7 @@ export class DynamicAppsExecutor {
.then(async (unsubscribe) => {
if (typeof unsubscribe !== "function") {
throw new DynamicAppsError(
- "agentos_apps_invalid_subscription",
+ "dynamic_apps_invalid_subscription",
"watchActiveRelease must resolve to an unsubscribe function",
);
}
@@ -574,13 +574,13 @@ export class DynamicAppsExecutor {
if (entry.epoch !== epoch) continue;
if (!resolution) {
throw new DynamicAppsError(
- "agentos_apps_not_deployed",
+ "dynamic_apps_not_deployed",
"app has no active direct release; call deployApp() first",
);
}
if (resolution.appId !== entry.appId) {
throw new DynamicAppsError(
- "agentos_apps_active_release_invalid",
+ "dynamic_apps_active_release_invalid",
"loadActiveRelease returned a release for a different app",
);
}
@@ -613,7 +613,7 @@ export class DynamicAppsExecutor {
): Promise {
if (this.#disposed) {
throw new DynamicAppsError(
- "agentos_apps_executor_disposed",
+ "dynamic_apps_executor_disposed",
"Dynamic Apps executor is shutting down",
);
}
@@ -645,7 +645,7 @@ export class DynamicAppsExecutor {
await this.#pruneCaches(true, artifact.byteLength);
if (artifact.byteLength > this.config.runtimeCacheMaxBytes) {
throw new DynamicAppsError(
- "agentos_apps_artifact_cache_limit",
+ "dynamic_apps_artifact_cache_limit",
"application artifact is larger than the configured runtime cache",
);
}
@@ -722,7 +722,7 @@ export class DynamicAppsExecutor {
}
if (this.#disposed) {
throw new DynamicAppsError(
- "agentos_apps_executor_disposed",
+ "dynamic_apps_executor_disposed",
"Dynamic Apps executor is shutting down",
);
}
@@ -853,7 +853,7 @@ export class DynamicAppsExecutor {
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new DynamicAppsError(
- "agentos_apps_execution_cancelled",
+ "dynamic_apps_execution_cancelled",
"application execution was cancelled",
);
}
@@ -1224,13 +1224,13 @@ function evaluationValue(result: EvaluationResult, timeoutMs: number): T {
}
if (result.outcome === "timed_out") {
throw new DynamicAppsError(
- "agentos_apps_execution_timeout",
+ "dynamic_apps_execution_timeout",
`application execution exceeded ${timeoutMs}ms`,
);
}
if (result.outcome === "cancelled") {
throw new DynamicAppsError(
- "agentos_apps_execution_cancelled",
+ "dynamic_apps_execution_cancelled",
"application execution was cancelled",
);
}
@@ -1241,7 +1241,7 @@ function evaluationValue(result: EvaluationResult, timeoutMs: number): T {
function disposedError(): DynamicAppsError {
return new DynamicAppsError(
- "agentos_apps_executor_disposed",
+ "dynamic_apps_executor_disposed",
"Dynamic Apps executor is shutting down",
);
}
@@ -1261,13 +1261,13 @@ function createReleaseLoadContext(
recordTiming(name, durationMs) {
if (!Number.isFinite(durationMs) || durationMs < 0) {
throw new DynamicAppsError(
- "agentos_apps_invalid_timing",
+ "dynamic_apps_invalid_timing",
"release load timing duration must be finite and non-negative",
);
}
if (typeof name !== "string" || Buffer.byteLength(name) > 64) {
throw new DynamicAppsError(
- "agentos_apps_invalid_timing",
+ "dynamic_apps_invalid_timing",
"release load timing name must be at most 64 bytes",
);
}
@@ -1277,7 +1277,7 @@ function createReleaseLoadContext(
.replace(/^-|-$/g, "");
if (!normalized) {
throw new DynamicAppsError(
- "agentos_apps_invalid_timing",
+ "dynamic_apps_invalid_timing",
"release load timing name must contain ASCII letters or digits",
);
}
@@ -1289,7 +1289,7 @@ function createReleaseLoadContext(
function verifyActiveRelease(input: ActiveRelease): ActiveRelease {
if (!input || typeof input !== "object") {
throw new DynamicAppsError(
- "agentos_apps_active_release_invalid",
+ "dynamic_apps_active_release_invalid",
"loadActiveRelease returned an invalid release",
);
}
@@ -1313,7 +1313,7 @@ function verifyActiveRelease(input: ActiveRelease): ActiveRelease {
input.maxResponseBytes < 1
) {
throw new DynamicAppsError(
- "agentos_apps_active_release_invalid",
+ "dynamic_apps_active_release_invalid",
"loadActiveRelease returned invalid release metadata",
);
}
@@ -1330,14 +1330,14 @@ function verifyActiveRelease(input: ActiveRelease): ActiveRelease {
typeof artifact.usesRivetKit !== "boolean"
) {
throw new DynamicAppsError(
- "agentos_apps_artifact_manifest_mismatch",
+ "dynamic_apps_artifact_manifest_mismatch",
"loadActiveRelease returned invalid artifact metadata",
);
}
const bytes = new Uint8Array(artifact.bytes);
if (createHash("sha256").update(bytes).digest("hex") !== artifact.hash) {
throw new DynamicAppsError(
- "agentos_apps_artifact_hash_mismatch",
+ "dynamic_apps_artifact_hash_mismatch",
"loaded artifact failed size or hash verification",
);
}
@@ -1368,7 +1368,7 @@ function validScaling(value: ActiveRelease["scaling"]): boolean {
async function serializeRequest(request: Request): Promise {
if (Buffer.byteLength(request.url) > MAX_URL_BYTES) {
throw new DynamicAppsError(
- "agentos_apps_request_limit",
+ "dynamic_apps_request_limit",
"request URL exceeds limit",
);
}
@@ -1377,7 +1377,7 @@ async function serializeRequest(request: Request): Promise {
!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(request.method)
) {
throw new DynamicAppsError(
- "agentos_apps_request_limit",
+ "dynamic_apps_request_limit",
"request method is invalid or exceeds limit",
);
}
@@ -1389,11 +1389,7 @@ async function serializeRequest(request: Request): Promise {
for (const name of [...HOP_BY_HOP_HEADERS, ...connectionTokens]) {
headers.delete(name);
}
- for (const name of [
- "x-rivet-token",
- "x-agentos-app-region",
- "x-agentos-app-registry-dispatch",
- ]) {
+ for (const name of ["x-rivet-token", "x-agentos-app-region"]) {
headers.delete(name);
}
const pairs = [...headers.entries()];
@@ -1429,7 +1425,7 @@ function responseFromEnvelope(
typeof envelope.bodyBase64 !== "string"
) {
throw new DynamicAppsError(
- "agentos_apps_invalid_response",
+ "dynamic_apps_invalid_response",
"application returned an invalid response envelope",
);
}
@@ -1440,7 +1436,7 @@ function responseFromEnvelope(
const body = Buffer.from(envelope.bodyBase64, "base64");
if (body.byteLength > MAX_RESPONSE_BODY_BYTES) {
throw new DynamicAppsError(
- "agentos_apps_response_limit",
+ "dynamic_apps_response_limit",
"application response exceeds limit",
);
}
@@ -1459,8 +1455,8 @@ function validateHeaderPairs(
if (pairs.length > MAX_HEADER_PAIRS) {
throw new DynamicAppsError(
kind === "request"
- ? "agentos_apps_request_limit"
- : "agentos_apps_response_header_limit",
+ ? "dynamic_apps_request_limit"
+ : "dynamic_apps_response_header_limit",
`${kind} headers exceed pair limit`,
);
}
@@ -1473,7 +1469,7 @@ function validateHeaderPairs(
typeof pair[1] !== "string"
) {
throw new DynamicAppsError(
- "agentos_apps_invalid_response",
+ "dynamic_apps_invalid_response",
`${kind} contains an invalid header pair`,
);
}
@@ -1482,8 +1478,8 @@ function validateHeaderPairs(
if (bytes > MAX_HEADER_BYTES) {
throw new DynamicAppsError(
kind === "request"
- ? "agentos_apps_request_limit"
- : "agentos_apps_response_header_limit",
+ ? "dynamic_apps_request_limit"
+ : "dynamic_apps_response_header_limit",
`${kind} headers exceed byte limit`,
);
}
@@ -1504,7 +1500,7 @@ async function readBoundedBody(
if (bytes > limit) {
await reader.cancel();
throw new DynamicAppsError(
- "agentos_apps_request_limit",
+ "dynamic_apps_request_limit",
"request body exceeds limit",
);
}
@@ -1551,7 +1547,7 @@ function integerEnv(
const value = Number(env[name] ?? fallback);
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
throw new DynamicAppsError(
- "agentos_apps_invalid_config",
+ "dynamic_apps_invalid_config",
`${name} must be an integer between ${minimum} and ${maximum}`,
);
}
@@ -1609,7 +1605,7 @@ class Semaphore {
}
if (this.#queue.length >= this.#maxQueued) {
throw new DynamicAppsError(
- "agentos_apps_no_capacity",
+ "dynamic_apps_no_capacity",
"Dynamic Apps execution queue is full",
);
}
@@ -1635,7 +1631,7 @@ class Semaphore {
if (offset >= 0) this.#queue.splice(offset, 1);
item.reject(
new DynamicAppsError(
- "agentos_apps_no_capacity",
+ "dynamic_apps_no_capacity",
`Dynamic Apps execution queue exceeded ${this.#waitMs}ms`,
),
);
diff --git a/packages/dynamic-apps-core/src/factory.ts b/packages/dynamic-apps-core/src/factory.ts
index 0da1e8951..1ce15e5c4 100644
--- a/packages/dynamic-apps-core/src/factory.ts
+++ b/packages/dynamic-apps-core/src/factory.ts
@@ -23,7 +23,7 @@ export function createDynamicApps(
typeof options.watchActiveRelease !== "function"
) {
throw new DynamicAppsError(
- "agentos_apps_invalid_config",
+ "dynamic_apps_invalid_config",
"createDynamicApps requires publishRelease, loadActiveRelease, and watchActiveRelease hooks",
);
}
@@ -98,7 +98,7 @@ export function createDynamicApps(
function disposedError(): DynamicAppsError {
return new DynamicAppsError(
- "agentos_apps_executor_disposed",
+ "dynamic_apps_executor_disposed",
"Dynamic Apps executor is shutting down",
);
}
diff --git a/packages/dynamic-apps-core/src/router.ts b/packages/dynamic-apps-core/src/router.ts
index d67220055..06c7b058b 100644
--- a/packages/dynamic-apps-core/src/router.ts
+++ b/packages/dynamic-apps-core/src/router.ts
@@ -37,10 +37,10 @@ function ordinaryRoutingError(error: unknown): Response | undefined {
}
const code = errorCode(error);
const message = error instanceof Error ? error.message : "";
- if (code === "agentos_apps_not_deployed") {
+ if (code === "dynamic_apps_not_deployed") {
return new Response("Dynamic App has no active release", { status: 503 });
}
- if (code === "agentos_apps_region_not_deployed") {
+ if (code === "dynamic_apps_region_not_deployed") {
const region = (error as { metadata?: { requestedRegion?: unknown } })
.metadata?.requestedRegion;
return new Response(
@@ -48,12 +48,12 @@ function ordinaryRoutingError(error: unknown): Response | undefined {
{ status: 421 },
);
}
- if (code === "agentos_apps_no_region") {
+ if (code === "dynamic_apps_no_region") {
return new Response("Dynamic App has no configured region", {
status: 503,
});
}
- if (code === "agentos_apps_request_limit") {
+ if (code === "dynamic_apps_request_limit") {
if (message.includes("URL")) {
return new Response("Request URL exceeds Dynamic Apps limit", {
status: 414,
@@ -83,20 +83,20 @@ function exceptionResponse(error: unknown): Response {
if (ordinary) return ordinary;
const code = errorCode(error);
const status =
- code === "agentos_apps_invalid_app_id"
+ code === "dynamic_apps_invalid_app_id"
? 400
- : code === "agentos_apps_not_deployed" ||
- code === "agentos_apps_region_not_deployed"
+ : code === "dynamic_apps_not_deployed" ||
+ code === "dynamic_apps_region_not_deployed"
? 404
- : code === "agentos_apps_request_limit"
+ : code === "dynamic_apps_request_limit"
? 413
- : code?.startsWith("agentos_apps_")
+ : code?.startsWith("dynamic_apps_")
? 503
: 500;
return Response.json(
{
error: {
- code: code ?? "agentos_apps_internal_error",
+ code: code ?? "dynamic_apps_internal_error",
message:
error instanceof Error
? error.message
diff --git a/packages/dynamic-apps-core/src/runtime.ts b/packages/dynamic-apps-core/src/runtime.ts
index 63303a803..84fc495e8 100644
--- a/packages/dynamic-apps-core/src/runtime.ts
+++ b/packages/dynamic-apps-core/src/runtime.ts
@@ -6,7 +6,7 @@ const MAX_FILE_PATH_BYTES = 1_024;
export const DIRECT_ENTRYPOINT = "direct-v2/main.mjs";
export const DIRECT_BUNDLE_PATH = "direct/main.mjs";
export const ACTOR_BUNDLE_PATH = "actor/main.mjs";
-export const DIRECT_RUNTIME_FORMAT = "agentos-apps-direct-v2";
+export const DIRECT_RUNTIME_FORMAT = "dynamic-apps-direct-v2";
export function normalizeAppPath(input: string): string {
if (typeof input !== "string" || input.length === 0 || input.includes("\0")) {
@@ -39,7 +39,7 @@ export function canonicalDeploymentHash(input: {
deploymentIdentity?: string;
}): string {
const hash = createHash("sha256");
- hash.update("agentos-apps-release-v19-mounted-hono-router\0");
+ hash.update("dynamic-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);
@@ -71,7 +71,15 @@ export function directRunnerSource(input: {
}): string {
const entrypoint = `./${normalizeAppPath(input.entrypoint)}`;
return `const dynamicAppsModuleImportStartedAt = performance.now();
-const application = await import(${JSON.stringify(entrypoint)});
+const dynamicAppsPreviousRuntimeMode = process.env.RIVETKIT_RUNTIME_MODE;
+delete process.env.RIVETKIT_RUNTIME_MODE;
+let application;
+try {
+ application = await import(${JSON.stringify(entrypoint)});
+} finally {
+ if (dynamicAppsPreviousRuntimeMode === undefined) delete process.env.RIVETKIT_RUNTIME_MODE;
+ else process.env.RIVETKIT_RUNTIME_MODE = dynamicAppsPreviousRuntimeMode;
+}
const dynamicAppsModuleImportMs = performance.now() - dynamicAppsModuleImportStartedAt;
const exported = application.default;
const appFetch = typeof exported === "function"
@@ -139,18 +147,15 @@ export async function dispatch(input) {
`;
}
-/** Host-owned wrapper for the app's mounted actor callback handler. */
+/** Initializes RivetKit WASM, then starts the application's own HTTP server. */
export function actorRunnerSource(entrypointInput: string): string {
const entrypoint = `./${normalizeAppPath(entrypointInput)}`;
- 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 handler = appFetch;
+ return `import { readFile } from "node:fs/promises";
+import initializeRivetKit from "@rivetkit/rivetkit-wasm";
+
+const wasmUrl = new URL(__AGENTOS_RIVETKIT_WASM_PATH__, import.meta.url);
+await initializeRivetKit({ module_or_path: await readFile(wasmUrl) });
+await import(${JSON.stringify(entrypoint)});
+console.log("DYNAMIC_APPS_SERVER_READY:" + (process.env.DYNAMIC_APPS_READY_NONCE ?? ""));
`;
}
diff --git a/packages/dynamic-apps-core/src/source.ts b/packages/dynamic-apps-core/src/source.ts
index 6142e41ac..741cbec2f 100644
--- a/packages/dynamic-apps-core/src/source.ts
+++ b/packages/dynamic-apps-core/src/source.ts
@@ -13,7 +13,7 @@ const APP_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62})$/;
export function validateAppId(appId: string): void {
if (!APP_ID_PATTERN.test(appId)) {
throw new DynamicAppsError(
- "agentos_apps_invalid_app_id",
+ "dynamic_apps_invalid_app_id",
"appId must be 1-63 lowercase letters, digits, or hyphens, beginning with a letter or digit",
{ appId },
);
@@ -29,21 +29,21 @@ function enforceFileBounds(
state.bytes += content.byteLength;
if (state.files > DEFAULT_MAX_FILES) {
throw new DynamicAppsError(
- "agentos_apps_file_limit",
+ "dynamic_apps_file_limit",
`application contains more than maxFiles ${DEFAULT_MAX_FILES}; reduce the source tree`,
{ limit: DEFAULT_MAX_FILES },
);
}
if (content.byteLength > DEFAULT_MAX_FILE_BYTES) {
throw new DynamicAppsError(
- "agentos_apps_file_size_limit",
+ "dynamic_apps_file_size_limit",
`${path} is ${content.byteLength} bytes, exceeding maxFileBytes ${DEFAULT_MAX_FILE_BYTES}; reduce the file size`,
{ path, observed: content.byteLength, limit: DEFAULT_MAX_FILE_BYTES },
);
}
if (state.bytes > DEFAULT_MAX_SOURCE_BYTES) {
throw new DynamicAppsError(
- "agentos_apps_source_limit",
+ "dynamic_apps_source_limit",
`application source exceeds maxSourceBytes ${DEFAULT_MAX_SOURCE_BYTES}; reduce the source tree`,
{ observed: state.bytes, limit: DEFAULT_MAX_SOURCE_BYTES },
);
@@ -53,7 +53,7 @@ function enforceFileBounds(
async function loadDirectory(source: URL): Promise> {
if (source.protocol !== "file:") {
throw new DynamicAppsError(
- "agentos_apps_invalid_source",
+ "dynamic_apps_invalid_source",
"deployApp source must be a file: directory URL",
{ protocol: source.protocol },
);
@@ -62,7 +62,7 @@ async function loadDirectory(source: URL): Promise> {
const rootStat = await lstat(root);
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
throw new DynamicAppsError(
- "agentos_apps_invalid_source",
+ "dynamic_apps_invalid_source",
"deployApp source must reference a real directory, not a symlink",
);
}
@@ -78,7 +78,7 @@ async function loadDirectory(source: URL): Promise> {
const entryStat = await lstat(path);
if (entryStat.isSymbolicLink()) {
throw new DynamicAppsError(
- "agentos_apps_source_symlink",
+ "dynamic_apps_source_symlink",
`application source contains unsupported symlink ${relative}`,
{ path: relative },
);
@@ -91,7 +91,7 @@ async function loadDirectory(source: URL): Promise> {
}
if (!entryStat.isFile()) {
throw new DynamicAppsError(
- "agentos_apps_source_file_type",
+ "dynamic_apps_source_file_type",
`application source contains unsupported file type ${relative}`,
{ path: relative },
);
diff --git a/packages/dynamic-apps-core/src/types.ts b/packages/dynamic-apps-core/src/types.ts
index c673a5220..e03b92e8f 100644
--- a/packages/dynamic-apps-core/src/types.ts
+++ b/packages/dynamic-apps-core/src/types.ts
@@ -23,7 +23,7 @@ export type DeployAppInput =
});
export interface ReleaseArtifact {
- format: "agentos-apps-direct-v2";
+ format: "dynamic-apps-direct-v2";
entrypoint: "direct-v2/main.mjs";
hash: string;
bytes: Uint8Array;
diff --git a/packages/dynamic-apps-core/tests/core.test.ts b/packages/dynamic-apps-core/tests/core.test.ts
index 6c598bf34..8a746bf12 100644
--- a/packages/dynamic-apps-core/tests/core.test.ts
+++ b/packages/dynamic-apps-core/tests/core.test.ts
@@ -182,7 +182,7 @@ function release(
appId,
release: `release-${name}`,
artifact: {
- format: "agentos-apps-direct-v2",
+ format: "dynamic-apps-direct-v2",
entrypoint: "direct-v2/main.mjs",
hash: createHash("sha256").update(bytes).digest("hex"),
bytes: new Uint8Array(bytes),
diff --git a/packages/dynamic-apps/API_CONTRACT.md b/packages/dynamic-apps/API_CONTRACT.md
index 4ac1343fa..c3f5b3651 100644
--- a/packages/dynamic-apps/API_CONTRACT.md
+++ b/packages/dynamic-apps/API_CONTRACT.md
@@ -89,7 +89,7 @@ interface PreparedDeployAppInput {
interface DeployAppOptions {
client?: {
- agentOSAppsApp: {
+ dynamicAppsApp: {
get?(key: string | string[]): {
deploy(
input: PreparedDeployAppInput,
@@ -198,16 +198,16 @@ export.
stored as `direct/main.mjs`; supported Node builtins resolve inside agentOS
and the bundle is validated before activation.
- Native Node addons and static-only output are rejected in the first preview.
-- A declared `rivetkit` dependency enables a second, platform-linked
- `actor/main.mjs` bundle. RivetKit itself is supplied by the host rather than
- installed into every app artifact. Both bundles must validate before the
- release can activate.
+- A declared `rivetkit` dependency enables a second self-contained
+ `actor/main.mjs` bundle with RivetKit's WebAssembly runtime. The actor bundle
+ runs inside agentOS; RivetKit and uploaded app code are not imported into the
+ host process. Both bundles must validate before the release can activate.
### Rivet and actor call
- The app state actor key is exactly `[input.appId]` and its client property
- remains `agentOSAppsApp` so the structural injected-client contract works.
-- If an injected client implements `agentOSAppsApp.get`, deployment first calls
+ remains `dynamicAppsApp` so the structural injected-client contract works.
+- If an injected client implements `dynamicAppsApp.get`, deployment first calls
`get([input.appId]).deploy(preparedInput)` so an existing stable actor is
resolved without a datacenter creation constraint. Only an actor-scoped
`not_found` falls back to
@@ -248,7 +248,7 @@ export.
metadata for direct HTTP and has no deleted scaler/replica effect.
- `endpoint`, `namespace`, deterministic `pool`, and an optional namespace-scoped
publishable `token` are returned. The pool is
- `agentos-apps-${sha256(appId).slice(0, 16)}`. Direct HTTP does not execute in
+ `dynamic-apps-${sha256(appId).slice(0, 16)}`. Direct HTTP does not execute in
that pool. Actor-enabled apps use it as their stable app actor runner pool.
- `createNamespace` remains source-compatible but is deprecated and ignored.
@@ -326,7 +326,7 @@ Thrown or rejected errors caught by the router use the existing JSON shape:
```json
{
"error": {
- "code": "agentos_apps_error_code",
+ "code": "dynamic_apps_error_code",
"message": "message"
}
}
@@ -336,11 +336,11 @@ The router preserves this exception mapping:
| Error | HTTP status |
| --- | ---: |
-| `agentos_apps_invalid_app_id` | 400 |
-| `agentos_apps_not_deployed` | 404 |
-| `agentos_apps_region_not_deployed` | 404 |
-| `agentos_apps_request_limit` | 413 |
-| other `agentos_apps_*` | 503 |
+| `dynamic_apps_invalid_app_id` | 400 |
+| `dynamic_apps_not_deployed` | 404 |
+| `dynamic_apps_region_not_deployed` | 404 |
+| `dynamic_apps_request_limit` | 413 |
+| other `dynamic_apps_*` | 503 |
| unknown/untyped error | 500 |
Replica identity/count/cold-start headers and benchmark timing headers are not
@@ -349,38 +349,47 @@ part of the retained API.
## App-defined RivetKit actor contract
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.
+`/api/rivet/*` in its default exported fetch router. When
+`RIVETKIT_RUNTIME_MODE=serverless`, it must also listen on the numeric `PORT`
+provided by Dynamic Apps. The same router serves ordinary direct requests. The
+server entrypoint should await its listening callback. Dynamic Apps treats
+successful module evaluation as readiness and does not poll an application
+health route.
On activation, deployment configures the returned `namespace` and `pool` with
-an authenticated serverless callback to the private `agentOSAppsApp` actor.
+an authenticated serverless callback to the private `dynamicAppsApp` actor.
Failure to configure that runner rolls the active pointer back. The callback
accepts only Rivet Engine traffic with the per-app secret and only the active
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 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
-pressure. They do not receive the deployment actor's control credential.
+The callback lazily verifies and mounts the release package in agentOS, then
+starts `actor/main.mjs` with `RIVETKIT_RUNTIME=wasm` and
+`RIVETKIT_RUNTIME_MODE=serverless`. Actor state, actions, events, and
+connections use the ordinary RivetKit protocol. The retained runtime count is
+an idle-cache target, not a callback admission limit; active callbacks are not
+rejected or evicted because the target is full. Guest runtimes do not receive
+the deployment actor's control credential.
+
+Rivet Engine's `/start` response is an SSE control stream. It contains the
+exact encoded runner ID/protocol version once, then keepalive pings, and stays
+open for the serverless runner lifespan; actor requests travel separately over
+RivetKit's outbound WebSocket. The host forwards the guest server's response
+through agentOS's native VM HTTP streaming API and propagates cancellation. The
+runner response is not mocked or serialized through process stdio.
Runner configuration uses the ordinary `RIVET_ENDPOINT` credential by default.
`DYNAMIC_APPS_CONTROL_TOKEN` can provide a separate token with datacenter-list
and runner-config create/update permissions; it is used only for those control
-calls and is never included in the actor worker environment.
+calls and is never included in the sandboxed actor environment.
-The actor worker uses `RIVET_PUBLIC_ENDPOINT` when present. If
+The sandboxed actor runtime uses `RIVET_PUBLIC_ENDPOINT` when present. If
`RIVET_ENDPOINT` contains credentials and no public endpoint is configured,
actor-enabled activation fails instead of forwarding the host secret. URL auth
is split into explicit endpoint, namespace, and publishable-token fields before
-the worker creates its RivetKit registry.
+the guest creates its RivetKit registry.
-Actor bundle execution shares a process with the host and is not a mutually
-hostile-code security boundary. Direct HTTP remains inside agentOS and never
-enters the actor worker.
+Both direct and actor bundle execution use agentOS as the hostile-code security
+boundary. Uploaded application modules are never evaluated by host Node.
## Deliberate runtime narrowing
@@ -421,29 +430,29 @@ execution-limit failures return the JSON exception shape; this is an explicit
replacement for the old mid-stream failure behavior.
An oversized response header/status envelope fails with
-`agentos_apps_response_header_limit` in the JSON exception shape. The agentOS
+`dynamic_apps_response_header_limit` in the JSON exception shape. The agentOS
request ABI enforces the logical body and header maxima before and after base64
expansion.
The retained deploy/source/control error codes are locked by golden tests,
-including `agentos_apps_invalid_app_id`, `agentos_apps_invalid_source`,
-`agentos_apps_source_symlink`, `agentos_apps_source_file_type`,
-`agentos_apps_file_limit`, `agentos_apps_file_size_limit`,
-`agentos_apps_source_limit`, `agentos_apps_invalid_files`,
-`agentos_apps_file_count_limit`, `agentos_apps_duplicate_file_path`,
-`agentos_apps_invalid_file`, `agentos_apps_entrypoint_not_found`,
-`agentos_apps_invalid_package_json`, `agentos_apps_dependency_limit`,
-`agentos_apps_invalid_regions`, `agentos_apps_invalid_region`,
-`agentos_apps_invalid_scaling`, `agentos_apps_invalid_config`,
-`agentos_apps_app_id_mismatch`, `agentos_apps_namespace_changed`,
-`agentos_apps_build_write_failed`, `agentos_apps_install_failed`,
-`agentos_apps_build_failed`, `agentos_apps_pack_failed`,
-`agentos_apps_build_artifact_size_limit`,
-`agentos_apps_build_artifact_truncated`,
-`agentos_apps_native_addon_unsupported`,
-`agentos_apps_control_response_limit`,
-`agentos_apps_namespace_lookup_failed`,
-`agentos_apps_namespace_create_failed`, and `host_registry_not_ready`.
+including `dynamic_apps_invalid_app_id`, `dynamic_apps_invalid_source`,
+`dynamic_apps_source_symlink`, `dynamic_apps_source_file_type`,
+`dynamic_apps_file_limit`, `dynamic_apps_file_size_limit`,
+`dynamic_apps_source_limit`, `dynamic_apps_invalid_files`,
+`dynamic_apps_file_count_limit`, `dynamic_apps_duplicate_file_path`,
+`dynamic_apps_invalid_file`, `dynamic_apps_entrypoint_not_found`,
+`dynamic_apps_invalid_package_json`, `dynamic_apps_dependency_limit`,
+`dynamic_apps_invalid_regions`, `dynamic_apps_invalid_region`,
+`dynamic_apps_invalid_scaling`, `dynamic_apps_invalid_config`,
+`dynamic_apps_app_id_mismatch`, `dynamic_apps_namespace_changed`,
+`dynamic_apps_build_write_failed`, `dynamic_apps_install_failed`,
+`dynamic_apps_build_failed`, `dynamic_apps_pack_failed`,
+`dynamic_apps_build_artifact_size_limit`,
+`dynamic_apps_build_artifact_truncated`,
+`dynamic_apps_native_addon_unsupported`,
+`dynamic_apps_control_response_limit`,
+`dynamic_apps_namespace_lookup_failed`,
+`dynamic_apps_namespace_create_failed`, and `host_registry_not_ready`.
## Mandatory verification and deletion gates
diff --git a/packages/dynamic-apps/README.md b/packages/dynamic-apps/README.md
index 1325abbe8..7e1356543 100644
--- a/packages/dynamic-apps/README.md
+++ b/packages/dynamic-apps/README.md
@@ -38,12 +38,14 @@ bodies are not supported.
## App-defined actors
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()`:
+router and starts an HTTP server on `PORT` when it runs in serverless mode.
+Dynamic Apps waits for the server entrypoint's listening callback before
+forwarding callbacks:
```ts
import { actor, setup } from "rivetkit";
import { Hono } from "hono";
+import { serve } from "@hono/node-server";
const counter = actor({
state: { value: 0 },
@@ -58,9 +60,25 @@ const registry = setup({ use: { counter } });
const app = new Hono();
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
app.all("*", () => new Response("ok"));
+if (process.env.RIVETKIT_RUNTIME_MODE === "serverless") {
+ await new Promise((resolve, reject) => {
+ const server = serve(
+ {
+ fetch: app.fetch,
+ port: Number(process.env.PORT),
+ hostname: "0.0.0.0",
+ },
+ resolve,
+ );
+ server.once("error", reject);
+ });
+}
export default app;
```
+Awaiting Hono's listening callback makes module completion the readiness signal;
+Dynamic Apps does not poll an application health route.
+
Use the unchanged `deployApp` result to create the app client:
```ts
@@ -99,16 +117,20 @@ 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 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.
+callback mounts its verified artifact in agentOS and runs the bundled RivetKit
+WebAssembly runtime in serverless mode. State, actions, events, connections,
+and streaming actor responses are handled by RivetKit inside the sandbox;
+ordinary HTTP for the same app uses the agentOS evaluation path.
+
+Rivet Engine requires `/api/rivet/start` to return a long-lived SSE control
+stream containing the real runner-init packet and keepalive pings. Actor traffic
+does not travel in this response; it uses RivetKit's outbound WebSocket. Dynamic
+Apps forwards the server's response with agentOS's native VM HTTP stream,
+including backpressure and cancellation. No app code executes in the host.
-Callback admission happens before the request body is read. The worker cache
-is a strict process limit across active and idle app bundles; a new bundle is
-rejected with `agentos_apps_no_capacity` when every worker slot is busy.
-Existing bundles continue sharing their worker up to the callback concurrency
-limit.
+Dynamic Apps does not impose a separate actor-callback concurrency gate. The
+configured actor runtime count controls retained idle entries only; active
+AgentOS runtimes are not rejected or evicted because that target is full.
## Host integration
@@ -121,14 +143,7 @@ import { Hono } from "hono";
const server = new Hono();
-const dispatchRegistry = (request: Request) => {
- const headers = new Headers(request.headers);
- headers.set("x-agentos-app-registry-dispatch", "1");
- return appsRouter.fetch(new Request(request, { headers }));
-};
-
-server.all("/api/rivet", (c) => dispatchRegistry(c.req.raw));
-server.all("/api/rivet/*", (c) => dispatchRegistry(c.req.raw));
+server.all("/api/rivet/*", (c) => appsRouter.fetch(c.req.raw));
server.route("/apps", appsRouter);
serve({ fetch: server.fetch, port: 3000 });
@@ -180,21 +195,18 @@ number of retained contexts remain idle afterward.
| `DYNAMIC_APPS_EXECUTION_TIMEOUT_MS` | `30000` |
| `DYNAMIC_APPS_ACTOR_WORKER_MAX_ENTRIES` | `4` |
| `DYNAMIC_APPS_ACTOR_WORKER_HEAP_LIMIT_MB` | `96` |
-| `DYNAMIC_APPS_ACTOR_WORKER_START_TIMEOUT_MS` | `10000` |
+| `DYNAMIC_APPS_ACTOR_WORKER_START_TIMEOUT_MS` | `30000` |
| `DYNAMIC_APPS_ACTOR_WORKER_IDLE_TTL_MS` | `30000` |
-| `DYNAMIC_APPS_ACTOR_START_PAYLOAD_MAX_BYTES` | `1048576` |
-| `DYNAMIC_APPS_ACTOR_REQUEST_CONCURRENCY` | `64` |
-| `DYNAMIC_APPS_ACTOR_REQUEST_QUEUE_SIZE` | `128` |
-| `DYNAMIC_APPS_ACTOR_REQUEST_QUEUE_WAIT_MS` | `5000` |
+| `DYNAMIC_APPS_ACTOR_START_PAYLOAD_MAX_BYTES` | `16777216` |
| `DYNAMIC_APPS_ACTOR_REQUEST_TIMEOUT_MS` | `30000` |
-Inside a finite cgroup, execution concurrency, both context-pool limits, and
-the actor-worker limit are upper bounds. At startup they are reduced when
+Inside a finite cgroup, direct execution concurrency and both direct
+context-pool limits are upper bounds. At startup they are reduced when
necessary to keep the configured heap cost below
`DYNAMIC_APPS_MEMORY_HIGH_WATER_PERCENT`, with host and payload headroom. They
are unchanged when the cgroup has no finite memory limit. Effective direct
-concurrency appears as `executionConcurrency` in executor diagnostics, and the
-effective actor limit appears as `workerLimit` in actor diagnostics.
+concurrency appears as `executionConcurrency` in executor diagnostics. The
+actor `workerLimit` diagnostic is the retained-idle target, not admission.
`DYNAMIC_APPS_CONTROL_TOKEN` optionally overrides only the token used by
Dynamic Apps Engine control-plane requests when running against a local or
@@ -223,7 +235,7 @@ setDynamicAppsLogHandler((event) => {
});
```
-The handler receives application stdout/stderr, actor worker output, build
+The handler receives application stdout/stderr, sandboxed actor output, build
phases, and runtime events. Enqueue into a logging SDK rather than performing a
blocking network request in the callback.
@@ -236,7 +248,6 @@ Successful pooled contexts are reset and reinitialized; failed, timed-out, or
aborted contexts are deleted.
agentOS supplies the filesystem, process, environment, and network permission
-boundary for direct HTTP. App actor code still runs in a bounded worker thread
-inside the host process. Run one trust domain per container and keep the host
-and agentOS runtime patched; do not treat actor workers as a boundary for
-mutually hostile tenants.
+boundary for both direct HTTP and app-defined actors. RivetKit actors use the
+bundled WASM runtime; uploaded app code is never imported into the Dynamic Apps
+host process.
diff --git a/packages/dynamic-apps/package.json b/packages/dynamic-apps/package.json
index e8bc01d3c..4b1848c9d 100644
--- a/packages/dynamic-apps/package.json
+++ b/packages/dynamic-apps/package.json
@@ -36,8 +36,8 @@
"rivetkit": "2.3.11"
},
"devDependencies": {
- "@rivet-dev/agentos-core": "0.2.15",
- "@rivet-dev/agentos-toolchain": "0.2.15",
+ "@rivet-dev/agentos-core": "0.2.16-rc.3",
+ "@rivet-dev/agentos-toolchain": "0.2.16-rc.3",
"@types/node": "^22.19.15",
"tsup": "^8.4.0",
"typescript": "^5.7.3",
diff --git a/packages/dynamic-apps/src/actor-runtime.ts b/packages/dynamic-apps/src/actor-runtime.ts
index 505f0f5f5..c595b522d 100644
--- a/packages/dynamic-apps/src/actor-runtime.ts
+++ b/packages/dynamic-apps/src/actor-runtime.ts
@@ -1,28 +1,21 @@
-import { createHash } from "node:crypto";
-import {
- mkdir,
- mkdtemp,
- readFile,
- rm,
- symlink,
- writeFile,
-} from "node:fs/promises";
-import { createRequire } from "node:module";
+import { createHash, randomUUID } from "node:crypto";
+import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
-import { dirname, join, posix } from "node:path";
-import { pathToFileURL } from "node:url";
-import { Worker } from "node:worker_threads";
-import {
- ACTOR_BUNDLE_PATH,
- capConcurrencyForMemory,
- DynamicAppsError,
- readCgroupMemory,
-} from "@rivet-dev/dynamic-apps-core/internal";
-import { DynamicAppsLogLineDecoder, emitDynamicAppsLog } from "./logging.js";
-
-const MAX_ACTOR_FILES = 4_096;
-const MAX_ACTOR_FILE_BYTES = 32 * 1024 * 1024;
-const MAX_ACTOR_ARTIFACT_BYTES = 64 * 1024 * 1024;
+import { join } from "node:path";
+import { AgentOs } from "@rivet-dev/agentos-core";
+import { emitDynamicAppsLog } from "./logging.js";
+
+const ACTOR_HTTP_PORT = 3000;
+const HOP_BY_HOP_HEADERS = [
+ "connection",
+ "keep-alive",
+ "proxy-authenticate",
+ "proxy-authorization",
+ "te",
+ "trailer",
+ "transfer-encoding",
+ "upgrade",
+] as const;
interface ActorRuntimeConfig {
maxEntries: number;
@@ -31,9 +24,6 @@ interface ActorRuntimeConfig {
idleTtlMs: number;
maxStartPayloadBytes: number;
memoryHighWaterPercent: number;
- requestConcurrency: number;
- requestQueueSize: number;
- requestQueueWaitMs: number;
requestTimeoutMs: number;
}
@@ -48,87 +38,47 @@ export interface ActorRuntimeRequest {
request: Request;
}
-interface PendingRequest {
- resolve(response: Response): void;
- reject(error: unknown): void;
- controller?: ReadableStreamDefaultController;
- waitingAck: boolean;
- settled: boolean;
- abort?: () => void;
- releaseAdmission(): void;
- timeout?: ReturnType;
-}
-
interface RuntimeEntry {
key: string;
appId: string;
release: string;
directory: string;
- worker: Worker;
- ready: Promise;
- pending: Map;
+ vm: AgentOs;
+ pid: number;
active: number;
lastUsedAt: number;
disposed: boolean;
- nextRequestId: number;
- logDisposers: Array<() => void>;
}
-type WorkerMessage =
- | { type: "ready" }
- | { type: "head"; id: string; status: number; headers: [string, string][] }
- | { type: "chunk"; id: string; chunk: Uint8Array }
- | { type: "end"; id: string }
- | { type: "error"; id?: string; message: string };
-
+/** Runs RivetKit callbacks in cached agentOS VMs using its native HTTP stream. */
export class DynamicActorRuntime {
readonly config: ActorRuntimeConfig;
readonly #entries = new Map();
readonly #creating = new Map>();
- readonly #admission: ActorAdmission;
readonly #timer: ReturnType;
- #workerReservations = 0;
#disposed = false;
#disposePromise?: Promise;
constructor(env: NodeJS.ProcessEnv = process.env) {
- const requestedMaxEntries = integerEnv(
- env,
- "DYNAMIC_APPS_ACTOR_WORKER_MAX_ENTRIES",
- 4,
- 1,
- 1_024,
- );
- const heapLimitMb = integerEnv(
- env,
- "DYNAMIC_APPS_ACTOR_WORKER_HEAP_LIMIT_MB",
- 96,
- 16,
- 2_048,
- );
- const memoryHighWaterPercent = integerEnv(
- env,
- "DYNAMIC_APPS_MEMORY_HIGH_WATER_PERCENT",
- 70,
- 10,
- 95,
- );
- const cgroupMemory = readCgroupMemory();
this.config = {
- maxEntries: cgroupMemory
- ? capConcurrencyForMemory({
- requested: requestedMaxEntries,
- contextAndVmLimitMb: heapLimitMb,
- memoryHighWaterPercent,
- currentBytes: cgroupMemory.currentBytes,
- maxBytes: cgroupMemory.maxBytes,
- })
- : requestedMaxEntries,
- heapLimitMb,
+ maxEntries: integerEnv(
+ env,
+ "DYNAMIC_APPS_ACTOR_WORKER_MAX_ENTRIES",
+ 4,
+ 1,
+ 1_024,
+ ),
+ heapLimitMb: integerEnv(
+ env,
+ "DYNAMIC_APPS_ACTOR_WORKER_HEAP_LIMIT_MB",
+ 96,
+ 16,
+ 2_048,
+ ),
startTimeoutMs: integerEnv(
env,
"DYNAMIC_APPS_ACTOR_WORKER_START_TIMEOUT_MS",
- 10_000,
+ 30_000,
10,
5 * 60_000,
),
@@ -142,31 +92,16 @@ export class DynamicActorRuntime {
maxStartPayloadBytes: integerEnv(
env,
"DYNAMIC_APPS_ACTOR_START_PAYLOAD_MAX_BYTES",
- 1024 * 1024,
- 1,
16 * 1024 * 1024,
- ),
- memoryHighWaterPercent,
- requestConcurrency: integerEnv(
- env,
- "DYNAMIC_APPS_ACTOR_REQUEST_CONCURRENCY",
- 64,
1,
- 4_096,
- ),
- requestQueueSize: integerEnv(
- env,
- "DYNAMIC_APPS_ACTOR_REQUEST_QUEUE_SIZE",
- 128,
- 0,
- 100_000,
+ 64 * 1024 * 1024,
),
- requestQueueWaitMs: integerEnv(
+ memoryHighWaterPercent: integerEnv(
env,
- "DYNAMIC_APPS_ACTOR_REQUEST_QUEUE_WAIT_MS",
- 5_000,
- 1,
- 60_000,
+ "DYNAMIC_APPS_MEMORY_HIGH_WATER_PERCENT",
+ 70,
+ 10,
+ 95,
),
requestTimeoutMs: integerEnv(
env,
@@ -176,11 +111,6 @@ export class DynamicActorRuntime {
5 * 60_000,
),
};
- this.#admission = new ActorAdmission(
- this.config.requestConcurrency,
- this.config.requestQueueSize,
- this.config.requestQueueWaitMs,
- );
this.#timer = setInterval(
() => void this.#prune(),
Math.min(this.config.idleTtlMs, 30_000),
@@ -189,92 +119,77 @@ export class DynamicActorRuntime {
}
async request(input: ActorRuntimeRequest): Promise {
- await this.#admission.acquire();
- let admissionHandedOff = false;
+ const body = await readBoundedBody(
+ input.request.body,
+ this.config.maxStartPayloadBytes,
+ );
+ if (!body)
+ return new Response("RivetKit actor start payload exceeds limit", {
+ status: 413,
+ });
+ const entry = await this.#entry(input);
+ const request = new Request(input.request.url, {
+ method: input.request.method,
+ headers: input.request.headers,
+ body:
+ input.request.method === "GET" || input.request.method === "HEAD"
+ ? undefined
+ : Buffer.from(body),
+ });
+ let head: Awaited>;
try {
- const body = await readBoundedBody(
- input.request.body,
- this.config.maxStartPayloadBytes,
- );
- if (!body) {
- return new Response("RivetKit actor start payload exceeds limit", {
- 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"),
- }),
+ head = await withTimeout(
+ entry.vm.fetchStreamStart(ACTOR_HTTP_PORT, request),
+ this.config.requestTimeoutMs,
+ `Dynamic App actor response headers exceeded ${this.config.requestTimeoutMs}ms`,
);
- const entry = await this.#entry(input);
- try {
- await entry.ready;
- } catch (error) {
- entry.active = Math.max(0, entry.active - 1);
- entry.lastUsedAt = Date.now();
- if (this.#entries.size > this.config.maxEntries) void this.#prune();
- throw error;
- }
- const id = String(++entry.nextRequestId);
- return new Promise((resolve, reject) => {
- const pending: PendingRequest = {
- resolve,
- reject,
- waitingAck: false,
- settled: false,
- releaseAdmission: () => this.#admission.release(),
- };
- const cancel = () => entry.worker.postMessage({ type: "cancel", id });
- pending.abort = cancel;
- entry.pending.set(id, pending);
- admissionHandedOff = true;
- pending.timeout = setTimeout(() => {
- emitDynamicAppsLog({
- level: "error",
- source: "runtime",
- message: "Dynamic App actor request timed out",
- appId: entry.appId,
- release: entry.release,
- });
- this.#failEntry(
- entry,
- new Error(
- `Dynamic App actor request exceeded ${this.config.requestTimeoutMs}ms`,
- ),
- );
- }, this.config.requestTimeoutMs);
+ } catch (error) {
+ this.#release(entry);
+ this.#failEntry(entry, error);
+ throw error;
+ }
+
+ let settled = false;
+ const settle = () => {
+ if (settled) return;
+ settled = true;
+ input.request.signal.removeEventListener("abort", cancel);
+ this.#release(entry);
+ };
+ const cancel = () => {
+ if (settled) return;
+ void entry.vm.fetchStreamCancel(head.streamId).catch(() => {});
+ settle();
+ };
+ input.request.signal.addEventListener("abort", cancel, { once: true });
+ if (input.request.signal.aborted) cancel();
+
+ const stream = new ReadableStream({
+ pull: async (controller) => {
+ if (settled) return controller.close();
try {
- entry.worker.postMessage({
- type: "request",
- id,
- method: input.request.method,
- url: input.request.url,
- headers: [...input.request.headers.entries()],
- body,
- });
- input.request.signal.addEventListener("abort", cancel, {
- once: true,
- });
- if (input.request.signal.aborted) cancel();
+ const chunk = await entry.vm.fetchStreamRead(head.streamId);
+ if (chunk.body.byteLength > 0) controller.enqueue(chunk.body);
+ if (chunk.done) {
+ controller.close();
+ settle();
+ }
} catch (error) {
- emitDynamicAppsLog({
- level: "error",
- source: "runtime",
- message: "Dynamic App actor worker transport failed",
- appId: entry.appId,
- release: entry.release,
- });
- this.#settle(entry, id);
- reject(error);
+ controller.error(error);
+ settle();
}
- });
- } finally {
- if (!admissionHandedOff) this.#admission.release();
- }
+ },
+ cancel: async () => {
+ if (settled) return;
+ await entry.vm.fetchStreamCancel(head.streamId).catch(() => {});
+ settle();
+ },
+ });
+ return new Response(stream, {
+ status: head.status,
+ statusText: head.statusText,
+ headers: proxyResponseHeaders(head.headers),
+ });
}
async invalidate(key: string): Promise {
@@ -286,18 +201,16 @@ export class DynamicActorRuntime {
diagnostics(): Record {
const entries = [...this.#entries.values()];
+ const active = entries.reduce((sum, entry) => sum + entry.active, 0);
return {
workerLimit: this.config.maxEntries,
entries: entries.length,
creating: this.#creating.size,
- workerReservations: this.#workerReservations,
- activeRequests: entries.reduce((sum, entry) => sum + entry.active, 0),
- pendingRequests: entries.reduce(
- (sum, entry) => sum + entry.pending.size,
- 0,
- ),
- admittedRequests: this.#admission.active,
- queuedRequests: this.#admission.queued,
+ workerReservations: 0,
+ activeRequests: active,
+ pendingRequests: active,
+ admittedRequests: active,
+ queuedRequests: 0,
};
}
@@ -305,16 +218,13 @@ export class DynamicActorRuntime {
if (this.#disposePromise !== undefined) return this.#disposePromise;
this.#disposed = true;
clearInterval(this.#timer);
- this.#admission.dispose();
this.#disposePromise = this.#finishDispose();
return this.#disposePromise;
}
async #finishDispose(): Promise {
- while (this.#creating.size > 0) {
+ while (this.#creating.size > 0)
await Promise.allSettled([...this.#creating.values()]);
- await new Promise((resolve) => setImmediate(resolve));
- }
await Promise.allSettled(
[...this.#entries.values()].map((entry) => this.#disposeEntry(entry)),
);
@@ -325,319 +235,179 @@ export class DynamicActorRuntime {
if (this.#disposed) throw new Error("Dynamic App actor runtime disposed");
const existing = this.#entries.get(input.key);
if (existing && !existing.disposed) return this.#lease(existing);
- const pending = this.#creating.get(input.key);
- if (pending) return this.#lease(await pending);
- const reservation = this.#reserveWorker();
- const promise = reservation.then(async () => {
- const entry = await this.#createEntry(input);
- if (this.#disposed) {
- await this.#disposeEntry(entry);
- throw new Error(
- "Dynamic App actor runtime disposed during worker creation",
- );
- }
- return entry;
- });
+ const creating = this.#creating.get(input.key);
+ if (creating) return this.#lease(await creating);
+ const promise = this.#createEntry(input);
this.#creating.set(input.key, promise);
- let reserved = true;
try {
const entry = await promise;
- this.#creating.delete(input.key);
- this.#workerReservations = Math.max(0, this.#workerReservations - 1);
- reserved = false;
if (this.#disposed) {
await this.#disposeEntry(entry);
- throw new Error(
- "Dynamic App actor runtime disposed during worker creation",
- );
+ throw new Error("Dynamic App actor runtime disposed during creation");
}
this.#entries.set(input.key, entry);
this.#lease(entry);
await this.#prune(entry.key);
return entry;
} finally {
- if (reserved) {
- this.#workerReservations = Math.max(0, this.#workerReservations - 1);
- }
- if (this.#creating.get(input.key) === promise) {
+ if (this.#creating.get(input.key) === promise)
this.#creating.delete(input.key);
- }
}
}
- #reserveWorker(): Promise {
- if (this.#disposed) throw new Error("Dynamic App actor runtime disposed");
- if (
- this.#entries.size + this.#workerReservations <
- this.config.maxEntries
- ) {
- this.#workerReservations += 1;
- return Promise.resolve();
- }
- const idle = [...this.#entries.values()]
- .filter((entry) => entry.active === 0 && !entry.disposed)
- .sort((a, b) => a.lastUsedAt - b.lastUsedAt)[0];
- if (!idle) {
- throw new DynamicAppsError(
- "agentos_apps_no_capacity",
- `Dynamic Apps actor worker limit ${this.config.maxEntries} is busy`,
- );
- }
- this.#entries.delete(idle.key);
- this.#workerReservations += 1;
- return this.#disposeEntry(idle);
- }
-
#lease(entry: RuntimeEntry): RuntimeEntry {
- if (entry.disposed) {
- throw new Error(
- "Dynamic App actor worker was disposed before request lease",
- );
- }
- entry.active += 1;
+ if (entry.disposed)
+ throw new Error("Dynamic App actor runtime was disposed");
+ entry.active++;
entry.lastUsedAt = Date.now();
return entry;
}
+ #release(entry: RuntimeEntry): void {
+ entry.active = Math.max(0, entry.active - 1);
+ entry.lastUsedAt = Date.now();
+ if (this.#entries.size > this.config.maxEntries) void this.#prune();
+ }
+
async #createEntry(input: ActorRuntimeRequest): Promise {
- const directory = await mkdtemp(join(tmpdir(), "dynamic-app-actor-"));
+ const directory = await mkdtemp(
+ join(tmpdir(), "dynamic-app-actor-agentos-"),
+ );
+ const artifactPath = join(directory, "release.aospkg");
+ let vm: AgentOs | undefined;
try {
- const files = extractActorFiles(await input.loadArtifact());
- for (const [path, bytes] of files) {
- const target = join(directory, ...path.split("/"));
- await mkdir(dirname(target), { recursive: true });
- await writeFile(target, bytes, { mode: 0o600 });
- }
- const rivetkitPackage = await resolvePlatformRivetKitPackage();
- await mkdir(join(directory, "node_modules"), { recursive: true });
- await symlink(
- rivetkitPackage,
- join(directory, "node_modules", "rivetkit"),
- "dir",
- );
- const entrypoint = pathToFileURL(join(directory, "main.mjs")).href;
- let readyResolve = () => {};
- let readyReject = (_error: unknown) => {};
- const ready = new Promise((resolve, reject) => {
- readyResolve = resolve;
- readyReject = reject;
+ await chmod(directory, 0o700);
+ await writeFile(artifactPath, await input.loadArtifact(), {
+ mode: 0o600,
});
- const worker = new Worker(
- new URL(
- `data:text/javascript,${encodeURIComponent(ACTOR_WORKER_SOURCE)}`,
- ),
- {
- workerData: { entrypoint },
- env: actorWorkerEnvironment(input),
- resourceLimits: {
- maxOldGenerationSizeMb: this.config.heapLimitMb,
- maxYoungGenerationSizeMb: Math.max(
- 4,
- Math.min(32, Math.floor(this.config.heapLimitMb / 4)),
- ),
+ vm = await AgentOs.create({
+ sidecar: { kind: "shared", pool: "dynamic-apps-actors" },
+ defaultSoftware: false,
+ loopbackExemptPorts: loopbackExemptPorts(input.endpoint),
+ mounts: [
+ {
+ path: "/app",
+ readOnly: true,
+ plugin: {
+ id: "agentos_packages",
+ config: {
+ kind: "tar",
+ tarPath: artifactPath,
+ root: "/",
+ readOnly: true,
+ },
+ },
},
- stdout: true,
- stderr: true,
+ ],
+ permissions: {
+ fs: "allow",
+ childProcess: "allow",
+ process: "allow",
+ env: "allow",
+ network: "allow",
},
- );
- const entry: RuntimeEntry = {
+ limits: { jsRuntime: { v8HeapLimitMb: this.config.heapLimitMb } },
+ });
+ const pendingLogs: Array<["stdout" | "stderr", Uint8Array]> = [];
+ const readyNonce = randomUUID();
+ const readyMarker = `DYNAMIC_APPS_SERVER_READY:${readyNonce}`;
+ let readyBuffer = "";
+ let resolveReady = () => {};
+ let rejectReady = (_error: unknown) => {};
+ const ready = new Promise((resolve, reject) => {
+ resolveReady = resolve;
+ rejectReady = reject;
+ });
+ let entry: RuntimeEntry | undefined;
+ const process = await vm.process.spawn("node", ["/app/actor/main.mjs"], {
+ cwd: "/app/actor",
+ env: stringEnvironment({
+ ...actorWorkerEnvironment(input),
+ DYNAMIC_APPS_READY_NONCE: readyNonce,
+ }),
+ onStdout: (data) => {
+ readyBuffer = `${readyBuffer}${new TextDecoder().decode(data)}`.slice(
+ -4_096,
+ );
+ if (readyBuffer.includes(readyMarker)) resolveReady();
+ if (entry) this.#logGuest(entry, "stdout", data);
+ else pendingLogs.push(["stdout", data]);
+ },
+ onStderr: (data) =>
+ entry
+ ? this.#logGuest(entry, "stderr", data)
+ : pendingLogs.push(["stderr", data]),
+ });
+ entry = {
key: input.key,
appId: input.appId ?? "unknown",
release: input.release ?? "unknown",
directory,
- worker,
- ready,
- pending: new Map(),
+ vm,
+ pid: process.pid,
active: 0,
lastUsedAt: Date.now(),
disposed: false,
- nextRequestId: 0,
- logDisposers: [],
};
- for (const stream of ["stdout", "stderr"] as const) {
- const output = worker[stream];
- const decoder = new DynamicAppsLogLineDecoder((message, truncated) =>
- emitDynamicAppsLog({
- level: stream === "stdout" ? "info" : "error",
- source: "actor",
- message,
- appId: input.appId ?? "unknown",
- release: input.release ?? "unknown",
- stream,
- ...(truncated ? { metadata: { truncated: true } } : {}),
- }),
- );
- const onData = (chunk: Uint8Array) => decoder.write(chunk);
- output.on("data", onData);
- entry.logDisposers.push(() => {
- output.off("data", onData);
- decoder.end();
- });
- }
- let startupSettled = false;
- const startupTimer = setTimeout(() => {
- if (startupSettled || entry.disposed) return;
- startupSettled = true;
+ for (const [stream, data] of pendingLogs)
+ this.#logGuest(entry, stream, data);
+ const processExit = vm.process.wait(process.pid);
+ void processExit.then((exit) => {
const error = new Error(
- `Dynamic App actor worker startup exceeded ${this.config.startTimeoutMs}ms`,
+ `Dynamic App agentOS actor process exited with ${exit.exitCode ?? 1}`,
);
- emitDynamicAppsLog({
- level: "error",
- source: "runtime",
- message: "Dynamic App actor worker startup timed out",
- appId: entry.appId,
- release: entry.release,
- });
- readyReject(error);
- this.#failEntry(entry, error);
- }, this.config.startTimeoutMs);
- startupTimer.unref?.();
- const finishStartup = () => {
- if (startupSettled) return;
- startupSettled = true;
- clearTimeout(startupTimer);
- };
- worker.on("message", (message: WorkerMessage) => {
- if (message.type === "ready") {
- finishStartup();
- readyResolve();
- return;
- }
- if (message.type === "error" && !message.id) {
- emitDynamicAppsLog({
- level: "error",
- source: "runtime",
- message: "Dynamic App actor worker reported a transport error",
- appId: entry.appId,
- release: entry.release,
- });
- finishStartup();
- readyReject(new Error(message.message));
- this.#failEntry(entry, new Error(message.message));
- return;
- }
- if (message.id) this.#handleMessage(entry, message);
- });
- worker.once("error", (error) => {
- emitDynamicAppsLog({
- level: "error",
- source: "runtime",
- message: "Dynamic App actor worker failed",
- appId: entry.appId,
- release: entry.release,
- });
- finishStartup();
- readyReject(error);
+ rejectReady(error);
+ if (!entry || entry.disposed) return;
this.#failEntry(entry, error);
});
- worker.once("exit", (code) => {
- finishStartup();
- if (!entry.disposed) {
- emitDynamicAppsLog({
- level: "error",
- source: "runtime",
- message: "Dynamic App actor worker exited",
- appId: entry.appId,
- release: entry.release,
- metadata: { exitCode: code },
- });
- const error = new Error(
- `Dynamic App actor worker exited with ${code}`,
- );
- readyReject(error);
- this.#failEntry(entry, error);
- }
- });
+ await withTimeout(
+ ready,
+ this.config.startTimeoutMs,
+ `Dynamic App agentOS actor startup exceeded ${this.config.startTimeoutMs}ms`,
+ );
return entry;
} catch (error) {
- await rm(directory, { recursive: true, force: true });
+ await vm?.dispose().catch(() => {});
+ await rm(directory, { recursive: true, force: true }).catch(() => {});
throw error;
}
}
- #handleMessage(entry: RuntimeEntry, message: WorkerMessage): void {
- if (!("id" in message) || !message.id) return;
- const pending = entry.pending.get(message.id);
- if (!pending || pending.settled) return;
- if (message.type === "head") {
- if (pending.timeout) {
- clearTimeout(pending.timeout);
- pending.timeout = undefined;
- }
- const stream = new ReadableStream({
- start: (controller) => {
- pending.controller = controller;
- },
- pull: () => {
- if (!pending.waitingAck) return;
- pending.waitingAck = false;
- entry.worker.postMessage({ type: "ack", id: message.id });
- },
- cancel: () => {
- entry.worker.postMessage({ type: "cancel", id: message.id });
- this.#settle(entry, message.id);
- },
- });
- pending.resolve(
- new Response(stream, {
- status: message.status,
- headers: message.headers,
- }),
- );
- return;
- }
- if (message.type === "chunk") {
- pending.controller?.enqueue(new Uint8Array(message.chunk));
- if ((pending.controller?.desiredSize ?? 1) > 0) {
- entry.worker.postMessage({ type: "ack", id: message.id });
- } else {
- pending.waitingAck = true;
- }
- return;
- }
- if (message.type === "end") {
- pending.controller?.close();
- this.#settle(entry, message.id);
- return;
- }
- if (message.type === "error") {
- const error = new Error(message.message);
- if (pending.controller) pending.controller.error(error);
- else pending.reject(error);
- this.#settle(entry, message.id);
- }
- }
-
- #settle(entry: RuntimeEntry, id: string): void {
- const pending = entry.pending.get(id);
- if (!pending || pending.settled) return;
- pending.settled = true;
- entry.pending.delete(id);
- if (pending.timeout) clearTimeout(pending.timeout);
- pending.releaseAdmission();
- entry.active = Math.max(0, entry.active - 1);
- entry.lastUsedAt = Date.now();
- if (this.#entries.size > this.config.maxEntries) void this.#prune();
- }
-
#failEntry(entry: RuntimeEntry, error: unknown): void {
this.#entries.delete(entry.key);
- for (const [id, pending] of entry.pending) {
- if (pending.controller) pending.controller.error(error);
- else pending.reject(error);
- this.#settle(entry, id);
- }
+ emitDynamicAppsLog({
+ level: "error",
+ source: "actor",
+ message:
+ error instanceof Error ? (error.stack ?? error.message) : String(error),
+ appId: entry.appId,
+ release: entry.release,
+ });
void this.#disposeEntry(entry);
}
+ #logGuest(
+ entry: Pick,
+ stream: "stdout" | "stderr",
+ data: Uint8Array,
+ ): void {
+ emitDynamicAppsLog({
+ level: stream === "stdout" ? "info" : "error",
+ source: "actor",
+ message: new TextDecoder().decode(data).slice(0, 64 * 1024),
+ appId: entry.appId,
+ release: entry.release,
+ stream,
+ });
+ }
+
async #prune(protectedKey?: string): Promise {
if (this.#disposed) return;
const now = Date.now();
const memoryPressure = await this.#memoryPressure();
- const entries = [...this.#entries.values()].sort(
+ for (const entry of [...this.#entries.values()].sort(
(a, b) => a.lastUsedAt - b.lastUsedAt,
- );
- for (const entry of entries) {
+ )) {
if (
entry.key !== protectedKey &&
entry.active === 0 &&
@@ -674,21 +444,15 @@ export class DynamicActorRuntime {
async #disposeEntry(entry: RuntimeEntry): Promise {
if (entry.disposed) return;
entry.disposed = true;
- for (const [id, pending] of entry.pending) {
- pending.reject(new Error("Dynamic App actor worker disposed"));
- pending.controller?.error(new Error("Dynamic App actor worker disposed"));
- this.#settle(entry, id);
- }
- for (const dispose of entry.logDisposers.splice(0)) dispose();
const results = await Promise.allSettled([
- entry.worker.terminate(),
+ entry.vm.dispose(),
rm(entry.directory, { recursive: true, force: true }),
]);
if (results.some((result) => result.status === "rejected")) {
emitDynamicAppsLog({
level: "error",
source: "runtime",
- message: "Dynamic App actor worker disposal failed",
+ message: "Dynamic App agentOS actor disposal failed",
appId: entry.appId,
release: entry.release,
});
@@ -696,6 +460,21 @@ export class DynamicActorRuntime {
}
}
+function proxyResponseHeaders(
+ input: Headers | Iterable,
+): Headers {
+ const headers = new Headers();
+ if (input instanceof Headers) {
+ input.forEach((value, name) => {
+ headers.append(name, value);
+ });
+ } else {
+ for (const [name, value] of input) headers.append(name, value);
+ }
+ for (const name of HOP_BY_HOP_HEADERS) headers.delete(name);
+ return headers;
+}
+
async function readBoundedBody(
body: ReadableStream | null,
limit: number,
@@ -720,40 +499,22 @@ async function readBoundedBody(
}
}
-let platformRivetKitPackage: Promise | undefined;
-
-function resolvePlatformRivetKitPackage(): Promise {
- platformRivetKitPackage ??= (async () => {
- const hostRequire = createRequire(import.meta.url);
- const rivetkitEntry = hostRequire.resolve("rivetkit");
- return findPackageRoot(rivetkitEntry);
- })();
- return platformRivetKitPackage;
-}
-
-async function findPackageRoot(entrypoint: string): Promise {
- let directory = dirname(entrypoint);
- for (;;) {
- try {
- const value = JSON.parse(
- await readFile(join(directory, "package.json"), "utf8"),
- ) as { name?: unknown };
- if (typeof value.name === "string" && value.name) return directory;
- } catch (error) {
- if (
- typeof error !== "object" ||
- error === null ||
- !("code" in error) ||
- error.code !== "ENOENT"
- ) {
- throw error;
- }
- }
- const parent = dirname(directory);
- if (parent === directory) {
- throw new Error(`Could not find package root for ${entrypoint}`);
- }
- directory = parent;
+async function withTimeout(
+ promise: Promise,
+ timeoutMs: number,
+ message: string,
+): Promise {
+ let timer: ReturnType | undefined;
+ try {
+ return await Promise.race([
+ promise,
+ new Promise((_, reject) => {
+ timer = setTimeout(() => reject(new Error(message)), timeoutMs);
+ timer.unref?.();
+ }),
+ ]);
+ } finally {
+ if (timer) clearTimeout(timer);
}
}
@@ -768,100 +529,52 @@ export function actorWorkerEnvironment(
const endpointToken = endpoint.password
? decodeURIComponent(endpoint.password)
: undefined;
- if (endpointNamespace && endpointNamespace !== input.namespace) {
+ if (endpointNamespace && endpointNamespace !== input.namespace)
throw new Error(
`Dynamic App actor endpoint namespace ${endpointNamespace} does not match ${input.namespace}`,
);
- }
endpoint.username = "";
endpoint.password = "";
return {
NODE_ENV: "production",
- RIVETKIT_RUNTIME: "native",
+ PORT: String(ACTOR_HTTP_PORT),
+ RIVETKIT_RUNTIME: "wasm",
RIVETKIT_RUNTIME_MODE: "serverless",
RIVET_ENDPOINT: endpoint.toString().replace(/\/$/u, ""),
RIVET_NAMESPACE: input.namespace,
...(endpointToken ? { RIVET_TOKEN: endpointToken } : {}),
RIVET_POOL: input.pool,
- RIVET_RUNNER: input.pool,
- RIVET_RUNNER_POOL: input.pool,
RIVET_ENVOY_VERSION: String(actorEnvoyVersion(input.key)),
+ ...(process.env.AGENTOS_DEBUG_HTTP_BRIDGE
+ ? { AGENTOS_DEBUG_HTTP_BRIDGE: process.env.AGENTOS_DEBUG_HTTP_BRIDGE }
+ : {}),
};
}
-function actorEnvoyVersion(key: string): number {
- return (
- createHash("sha256").update(key).digest().readUInt32BE(0) & 0x7fffffff || 1
+function stringEnvironment(env: NodeJS.ProcessEnv): Record {
+ return Object.fromEntries(
+ Object.entries(env).filter(
+ (entry): entry is [string, string] => typeof entry[1] === "string",
+ ),
);
}
-function extractActorFiles(artifact: Uint8Array): Map {
- const buffer = Buffer.from(
- artifact.buffer,
- artifact.byteOffset,
- artifact.byteLength,
- );
+function loopbackExemptPorts(endpoint: string): number[] {
+ const url = new URL(endpoint);
if (
- buffer.byteLength < 16 ||
- buffer[0] !== 137 ||
- buffer.subarray(1, 4).toString("ascii") !== "AOS"
+ url.hostname !== "127.0.0.1" &&
+ url.hostname !== "localhost" &&
+ url.hostname !== "[::1]"
) {
- throw new Error("Dynamic App actor artifact is not an AOSP package");
+ return [];
}
- let offset = 16 + buffer.readUInt32LE(8) + buffer.readUInt32LE(12);
- let total = 0;
- const output = new Map();
- while (offset + 512 <= buffer.byteLength) {
- const header = buffer.subarray(offset, offset + 512);
- if (header.every((value) => value === 0)) break;
- const name = tarString(header.subarray(0, 100));
- const prefix = tarString(header.subarray(345, 500));
- const path = `${prefix ? `${prefix}/` : ""}${name}`.replace(/^\.\//, "");
- const size = Number.parseInt(
- tarString(header.subarray(124, 136)) || "0",
- 8,
- );
- const type = String.fromCharCode(header[156] ?? 0);
- if (!Number.isSafeInteger(size) || size < 0) {
- throw new Error("Dynamic App actor artifact has an invalid tar entry");
- }
- const dataOffset = offset + 512;
- const next = dataOffset + Math.ceil(size / 512) * 512;
- if (next > buffer.byteLength) {
- throw new Error("Dynamic App actor artifact is truncated");
- }
- if (path.startsWith("actor/") && (type === "\0" || type === "0")) {
- const relative = posix.normalize(path.slice("actor/".length));
- if (
- !relative ||
- relative === "." ||
- relative === ".." ||
- relative.startsWith("../") ||
- posix.isAbsolute(relative) ||
- size > MAX_ACTOR_FILE_BYTES
- ) {
- throw new Error("Dynamic App actor artifact contains an invalid path");
- }
- total += size;
- if (total > MAX_ACTOR_ARTIFACT_BYTES || output.size >= MAX_ACTOR_FILES) {
- throw new Error("Dynamic App actor artifact exceeds extraction limits");
- }
- output.set(
- relative,
- new Uint8Array(buffer.subarray(dataOffset, dataOffset + size)),
- );
- }
- offset = next;
- }
- if (!output.has(ACTOR_BUNDLE_PATH.slice("actor/".length))) {
- throw new Error("Dynamic App artifact has no actor bundle");
- }
- return output;
+ return [Number(url.port || (url.protocol === "https:" ? 443 : 80))];
}
-function tarString(bytes: Uint8Array): string {
- const end = bytes.indexOf(0);
- return Buffer.from(end < 0 ? bytes : bytes.subarray(0, end)).toString("utf8");
+function actorEnvoyVersion(key: string): number {
+ return (
+ createHash("sha256").update(key).digest().readUInt32BE(0) & 0x7fffffff || 1
+ );
}
function integerEnv(
@@ -872,160 +585,11 @@ function integerEnv(
maximum: number,
): number {
const value = Number(env[name] ?? fallback);
- if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum)
throw new Error(`${name} must be an integer from ${minimum} to ${maximum}`);
- }
return value;
}
-class ActorAdmission {
- readonly capacity: number;
- readonly #maxQueued: number;
- readonly #waitMs: number;
- #active = 0;
- #disposed = false;
- #queue: Array<{ resolve(): void; reject(error: unknown): void }> = [];
-
- constructor(capacity: number, maxQueued: number, waitMs: number) {
- this.capacity = capacity;
- this.#maxQueued = maxQueued;
- this.#waitMs = waitMs;
- }
-
- get active(): number {
- return this.#active;
- }
-
- get queued(): number {
- return this.#queue.length;
- }
-
- async acquire(): Promise {
- if (this.#disposed) throw new Error("actor runtime disposed");
- if (this.#active < this.capacity) {
- this.#active += 1;
- return;
- }
- if (this.#queue.length >= this.#maxQueued) {
- throw new DynamicAppsError(
- "agentos_apps_no_capacity",
- "Dynamic Apps actor callback queue is full",
- );
- }
- await new Promise((resolve, reject) => {
- let settled = false;
- const item = {
- resolve: () => {
- if (settled) return;
- settled = true;
- clearTimeout(timer);
- this.#active += 1;
- resolve();
- },
- reject: (error: unknown) => {
- if (settled) return;
- settled = true;
- clearTimeout(timer);
- reject(error);
- },
- };
- const timer = setTimeout(() => {
- const offset = this.#queue.indexOf(item);
- if (offset >= 0) this.#queue.splice(offset, 1);
- item.reject(
- new DynamicAppsError(
- "agentos_apps_no_capacity",
- `Dynamic Apps actor callback queue exceeded ${this.#waitMs}ms`,
- ),
- );
- }, this.#waitMs);
- this.#queue.push(item);
- });
- }
-
- release(): void {
- if (this.#active <= 0) return;
- this.#active -= 1;
- this.#queue.shift()?.resolve();
- }
-
- dispose(): void {
- this.#disposed = true;
- for (const item of this.#queue.splice(0)) {
- item.reject(new Error("actor runtime disposed"));
- }
- }
-}
-
-const ACTOR_WORKER_SOURCE = `
-import { parentPort, workerData } from "node:worker_threads";
-if (!parentPort) throw new Error("Dynamic App actor worker has no parent port");
-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));
-const finishAck = (id) => { const resolve = acknowledgements.get(id); acknowledgements.delete(id); resolve?.(); };
-const describeError = (error) => {
- if (!(error instanceof Error)) return String(error);
- const details = [error.stack || error.message];
- let cause = error.cause;
- for (let depth = 0; cause !== undefined && depth < 4; depth += 1) {
- details.push("Caused by: " + (cause instanceof Error ? cause.stack || cause.message : String(cause)));
- cause = cause instanceof Error ? cause.cause : undefined;
- }
- return details.join("\\n");
-};
-parentPort.on("message", (message) => {
- if (message.type === "ack") { finishAck(message.id); return; }
- if (message.type === "cancel") {
- requests.get(message.id)?.abort(new Error("host cancelled actor callback"));
- finishAck(message.id);
- return;
- }
- if (message.type !== "request") return;
- void (async () => {
- const controller = new AbortController();
- requests.set(message.id, controller);
- try {
- const response = await handler(new Request(message.url, {
- method: message.method,
- headers: message.headers,
- body: message.method === "GET" || message.method === "HEAD" ? undefined : message.body,
- signal: controller.signal,
- }));
- parentPort.postMessage({
- type: "head",
- id: message.id,
- status: response.status,
- headers: [...response.headers.entries()],
- });
- if (response.body) {
- const reader = response.body.getReader();
- try {
- for (;;) {
- const chunk = await reader.read();
- if (chunk.done) break;
- const bytes = new Uint8Array(chunk.value);
- parentPort.postMessage({ type: "chunk", id: message.id, chunk: bytes }, [bytes.buffer]);
- await waitForAck(message.id);
- }
- } finally {
- reader.releaseLock();
- }
- }
- parentPort.postMessage({ type: "end", id: message.id });
- } catch (error) {
- parentPort.postMessage({ type: "error", id: message.id, message: describeError(error) });
- } finally {
- requests.delete(message.id);
- finishAck(message.id);
- }
- })();
-});
-parentPort.postMessage({ type: "ready" });
-`;
-
let defaultActorRuntime: DynamicActorRuntime | undefined;
export function getDefaultActorRuntime(): DynamicActorRuntime {
diff --git a/packages/dynamic-apps/src/actors.ts b/packages/dynamic-apps/src/actors.ts
index 1cc2cbca7..de5da8bb7 100644
--- a/packages/dynamic-apps/src/actors.ts
+++ b/packages/dynamic-apps/src/actors.ts
@@ -30,7 +30,9 @@ const DEFAULT_MAX_REGIONS = 8;
const DEFAULT_MAX_REQUEST_BYTES = 1024 * 1024;
const DEFAULT_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
const MAX_REPLICAS = 128;
-export const ARTIFACT_CHUNK_BYTES = 512 * 1024;
+// Keep the raw payload comfortably below the Engine action-message limit after
+// Uint8Array JSON/base64 serialization and protocol framing.
+export const ARTIFACT_CHUNK_BYTES = 128 * 1024;
const MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
const MAX_ARTIFACT_CHUNKS = Math.ceil(
MAX_ARTIFACT_BYTES / ARTIFACT_CHUNK_BYTES,
@@ -65,6 +67,7 @@ export interface StoredAppRelease extends AppReleaseInfo {
export interface AppState {
activeRelease: string | null;
+ activatingRelease?: string | null;
namespace: string | null;
revision: number;
cloudNamespace?: string | null;
@@ -75,7 +78,7 @@ export interface AppState {
}
export interface DynamicAppsActors {
- agentOSAppsApp: AnyActorDefinition;
+ dynamicAppsApp: AnyActorDefinition;
}
interface BeginReleasePublishInput {
@@ -152,7 +155,7 @@ async function actorBoundary(run: () => Promise): Promise {
function positiveInteger(value: number, name: string, maximum: number): number {
if (!Number.isInteger(value) || value < 1 || value > maximum) {
fail(
- "agentos_apps_invalid_config",
+ "dynamic_apps_invalid_config",
`${name} must be an integer between 1 and ${maximum}`,
{ name, maximum },
);
@@ -167,7 +170,7 @@ export function normalizeScaling(
input !== undefined &&
(typeof input !== "object" || input === null || Array.isArray(input))
) {
- fail("agentos_apps_invalid_scaling", "scaling must be an object");
+ fail("dynamic_apps_invalid_scaling", "scaling must be an object");
}
const minReplicas = input?.minReplicas ?? 0;
const maxReplicas = input?.maxReplicas ?? 128;
@@ -178,7 +181,7 @@ export function normalizeScaling(
minReplicas > MAX_REPLICAS
) {
fail(
- "agentos_apps_invalid_scaling",
+ "dynamic_apps_invalid_scaling",
`scaling.minReplicas must be an integer between 0 and ${MAX_REPLICAS}`,
);
}
@@ -186,7 +189,7 @@ export function normalizeScaling(
positiveInteger(targetConcurrency, "scaling.targetConcurrency", 1_024);
if (minReplicas > maxReplicas) {
fail(
- "agentos_apps_invalid_scaling",
+ "dynamic_apps_invalid_scaling",
"scaling.minReplicas cannot exceed scaling.maxReplicas",
);
}
@@ -198,19 +201,19 @@ function normalizeRegions(
fallbackRegion: string,
): string[] {
if (regions !== undefined && !Array.isArray(regions)) {
- fail("agentos_apps_invalid_regions", "regions must be an array");
+ fail("dynamic_apps_invalid_regions", "regions must be an array");
}
const unique = [...new Set(regions ?? [fallbackRegion || "default"])];
if (unique.length === 0 || unique.length > DEFAULT_MAX_REGIONS) {
fail(
- "agentos_apps_invalid_regions",
+ "dynamic_apps_invalid_regions",
`an app must have between 1 and ${DEFAULT_MAX_REGIONS} regions`,
);
}
for (const region of unique) {
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(region)) {
fail(
- "agentos_apps_invalid_region",
+ "dynamic_apps_invalid_region",
`invalid region ${JSON.stringify(region)}`,
);
}
@@ -220,7 +223,7 @@ function normalizeRegions(
export async function migrateAppsTables(database: RawAccess): Promise {
await database.execute(`
- CREATE TABLE IF NOT EXISTS agentos_apps_releases (
+ CREATE TABLE IF NOT EXISTS dynamic_apps_releases (
release_id TEXT PRIMARY KEY,
created_at INTEGER NOT NULL,
status TEXT NOT NULL,
@@ -238,7 +241,7 @@ export async function migrateAppsTables(database: RawAccess): Promise {
uses_rivetkit INTEGER NOT NULL DEFAULT 0
CHECK (uses_rivetkit IN (0, 1))
);
- CREATE TABLE IF NOT EXISTS agentos_apps_release_files (
+ CREATE TABLE IF NOT EXISTS dynamic_apps_release_files (
release_id TEXT NOT NULL,
path TEXT NOT NULL,
chunk_index INTEGER NOT NULL,
@@ -246,28 +249,28 @@ export async function migrateAppsTables(database: RawAccess): Promise {
byte_length INTEGER NOT NULL,
PRIMARY KEY (release_id, path, chunk_index)
);
- CREATE TABLE IF NOT EXISTS agentos_apps_artifact_chunks (
+ CREATE TABLE IF NOT EXISTS dynamic_apps_artifact_chunks (
release_id TEXT NOT NULL,
chunk_index INTEGER NOT NULL,
content BLOB NOT NULL,
byte_length INTEGER NOT NULL,
PRIMARY KEY (release_id, chunk_index)
);
- CREATE INDEX IF NOT EXISTS idx_agentos_apps_releases_created_at
- ON agentos_apps_releases(created_at);
+ CREATE INDEX IF NOT EXISTS idx_dynamic_apps_releases_created_at
+ ON dynamic_apps_releases(created_at);
`);
const columns = await database.execute<{ name: string }>(
- "PRAGMA table_info(agentos_apps_releases)",
+ "PRAGMA table_info(dynamic_apps_releases)",
);
if (!columns.some((column) => column.name === "callback_secret")) {
await database.execute(
- `ALTER TABLE agentos_apps_releases
+ `ALTER TABLE dynamic_apps_releases
ADD COLUMN callback_secret TEXT NOT NULL DEFAULT ''`,
);
}
if (!columns.some((column) => column.name === "uses_rivetkit")) {
await database.execute(
- `ALTER TABLE agentos_apps_releases
+ `ALTER TABLE dynamic_apps_releases
ADD COLUMN uses_rivetkit INTEGER NOT NULL DEFAULT 0
CHECK (uses_rivetkit IN (0, 1))`,
);
@@ -280,21 +283,21 @@ async function deleteReleaseFilesBatched(
): Promise {
for (let batch = 0; batch < MAX_SOURCE_CHUNKS; batch += 1) {
const rows = await database.execute<{ chunks: number }>(
- `SELECT COUNT(*) AS chunks FROM agentos_apps_release_files
+ `SELECT COUNT(*) AS chunks FROM dynamic_apps_release_files
WHERE release_id = ?`,
releaseId,
);
if (Number(rows[0]?.chunks ?? 0) === 0) return;
await database.execute(
- `DELETE FROM agentos_apps_release_files WHERE rowid IN (
- SELECT rowid FROM agentos_apps_release_files
+ `DELETE FROM dynamic_apps_release_files WHERE rowid IN (
+ SELECT rowid FROM dynamic_apps_release_files
WHERE release_id = ? ORDER BY path, chunk_index LIMIT 1
)`,
releaseId,
);
}
fail(
- "agentos_apps_source_cleanup_limit",
+ "dynamic_apps_source_cleanup_limit",
`source cleanup exceeded ${MAX_SOURCE_CHUNKS} bounded batches`,
);
}
@@ -315,7 +318,7 @@ async function persistReleaseFilesBatched(
(index + 1) * SOURCE_CHUNK_BYTES,
);
await database.execute(
- `INSERT INTO agentos_apps_release_files
+ `INSERT INTO dynamic_apps_release_files
(release_id, path, chunk_index, content, byte_length)
VALUES (?, ?, ?, ?, ?)`,
releaseId,
@@ -334,21 +337,21 @@ async function deleteArtifactChunksBatched(
): Promise {
for (let batch = 0; batch < MAX_ARTIFACT_CHUNKS; batch += 1) {
const rows = await database.execute<{ chunks: number }>(
- `SELECT COUNT(*) AS chunks FROM agentos_apps_artifact_chunks
+ `SELECT COUNT(*) AS chunks FROM dynamic_apps_artifact_chunks
WHERE release_id = ?`,
releaseId,
);
if (Number(rows[0]?.chunks ?? 0) === 0) return;
await database.execute(
- `DELETE FROM agentos_apps_artifact_chunks WHERE rowid IN (
- SELECT rowid FROM agentos_apps_artifact_chunks
+ `DELETE FROM dynamic_apps_artifact_chunks WHERE rowid IN (
+ SELECT rowid FROM dynamic_apps_artifact_chunks
WHERE release_id = ? ORDER BY chunk_index LIMIT 1
)`,
releaseId,
);
}
fail(
- "agentos_apps_artifact_cleanup_limit",
+ "dynamic_apps_artifact_cleanup_limit",
`artifact cleanup exceeded ${MAX_ARTIFACT_CHUNKS} bounded batches`,
);
}
@@ -394,7 +397,7 @@ async function getStoredRelease(
releaseId: string,
): Promise {
const rows = await database.execute(
- "SELECT * FROM agentos_apps_releases WHERE release_id = ?",
+ "SELECT * FROM dynamic_apps_releases WHERE release_id = ?",
releaseId,
);
return rows[0] ? releaseFromRow(rows[0]) : undefined;
@@ -404,7 +407,7 @@ async function listStoredReleases(
database: RawAccess,
): Promise {
const rows = await database.execute(
- "SELECT * FROM agentos_apps_releases ORDER BY created_at ASC",
+ "SELECT * FROM dynamic_apps_releases ORDER BY created_at ASC",
);
return rows.map(releaseFromRow);
}
@@ -419,7 +422,7 @@ async function readStoredArtifact(
byte_length: number;
}>(
`SELECT chunk_index, content, byte_length
- FROM agentos_apps_artifact_chunks
+ FROM dynamic_apps_artifact_chunks
WHERE release_id = ? ORDER BY chunk_index ASC`,
release.release,
);
@@ -432,7 +435,7 @@ async function readStoredArtifact(
rows.length > MAX_ARTIFACT_CHUNKS
) {
fail(
- "agentos_apps_artifact_manifest_invalid",
+ "dynamic_apps_artifact_manifest_invalid",
`artifact ${release.release} has an invalid chunk count`,
);
}
@@ -453,7 +456,7 @@ async function readStoredArtifact(
content.byteLength !== expected
) {
fail(
- "agentos_apps_artifact_manifest_invalid",
+ "dynamic_apps_artifact_manifest_invalid",
`artifact ${release.release} contains an invalid chunk`,
);
}
@@ -466,7 +469,7 @@ async function readStoredArtifact(
createHash("sha256").update(artifact).digest("hex") !== release.artifactHash
) {
fail(
- "agentos_apps_artifact_hash_mismatch",
+ "dynamic_apps_artifact_hash_mismatch",
`artifact ${release.release} failed hash verification`,
);
}
@@ -515,7 +518,7 @@ function actorPublicEndpoint(
const endpoint = new URL(raw);
if (!publicEndpoint && (endpoint.username || endpoint.password)) {
throw new DynamicAppsError(
- "agentos_apps_public_endpoint_required",
+ "dynamic_apps_public_endpoint_required",
"RIVET_PUBLIC_ENDPOINT is required to run app actors when RIVET_ENDPOINT contains credentials",
);
}
@@ -530,7 +533,7 @@ function validateBeginInput(
const appId = c.key[0];
if (!appId || c.key.length !== 1 || input.appId !== appId) {
fail(
- "agentos_apps_app_id_mismatch",
+ "dynamic_apps_app_id_mismatch",
"deployApp appId must match the stable application actor key",
{ appId: input.appId, actorKey: c.key },
);
@@ -547,7 +550,7 @@ function validateBeginInput(
!Number.isSafeInteger(input.createdAt) ||
input.createdAt < 0
) {
- fail("agentos_apps_publish_invalid", "release publish metadata is invalid");
+ fail("dynamic_apps_publish_invalid", "release publish metadata is invalid");
}
return appId;
}
@@ -563,7 +566,7 @@ function releaseIdFor(input: {
usesRivetKit: boolean;
}): string {
const hash = createHash("sha256");
- hash.update("agentos-apps-rivet-release-v1\0");
+ hash.update("dynamic-apps-rivet-release-v1\0");
for (const value of [
input.buildId,
input.artifactHash,
@@ -644,7 +647,7 @@ async function beginReleasePublishLocked(
}
await deleteArtifactChunksBatched(c.db, releaseId);
await c.db.execute(
- `INSERT INTO agentos_apps_releases (
+ `INSERT INTO dynamic_apps_releases (
release_id, created_at, status, entrypoint,
artifact_hash, artifact_bytes, build_error,
regions_json, scaling_json, namespace, envoy_version,
@@ -697,14 +700,14 @@ async function writeReleaseChunk(
!(input.content instanceof Uint8Array)
) {
fail(
- "agentos_apps_invalid_artifact_chunk",
+ "dynamic_apps_invalid_artifact_chunk",
"release chunk metadata is invalid or superseded",
);
}
const release = await getStoredRelease(c.db, input.release);
if (!release || release.status !== "building") {
fail(
- "agentos_apps_artifact_not_building",
+ "dynamic_apps_artifact_not_building",
`release ${input.release} is not accepting artifact chunks`,
);
}
@@ -715,13 +718,13 @@ async function writeReleaseChunk(
: ARTIFACT_CHUNK_BYTES;
if (input.index >= chunks || input.content.byteLength !== expected) {
fail(
- "agentos_apps_invalid_artifact_chunk",
+ "dynamic_apps_invalid_artifact_chunk",
`artifact chunk ${input.index} has an invalid length`,
);
}
const content = new Uint8Array(input.content);
await c.db.execute(
- `INSERT INTO agentos_apps_artifact_chunks
+ `INSERT INTO dynamic_apps_artifact_chunks
(release_id, chunk_index, content, byte_length)
VALUES (?, ?, ?, ?)
ON CONFLICT(release_id, chunk_index) DO UPDATE SET
@@ -744,7 +747,7 @@ async function commitReleasePublishLocked(
input.sequence !== state.latestPublishSequence
) {
fail(
- "agentos_apps_publish_superseded",
+ "dynamic_apps_publish_superseded",
"a newer release publish superseded this upload",
);
}
@@ -754,7 +757,7 @@ async function commitReleasePublishLocked(
(release.status !== "building" && release.status !== "ready")
) {
fail(
- "agentos_apps_artifact_not_ready",
+ "dynamic_apps_artifact_not_ready",
`release ${input.release} cannot be committed`,
);
}
@@ -768,7 +771,7 @@ async function commitReleasePublishLocked(
input.chunks > MAX_ARTIFACT_CHUNKS
) {
fail(
- "agentos_apps_artifact_manifest_invalid",
+ "dynamic_apps_artifact_manifest_invalid",
"release commit has an invalid artifact chunk count",
);
}
@@ -778,7 +781,7 @@ async function commitReleasePublishLocked(
}
if (release.status !== "ready") {
await c.db.execute(
- `UPDATE agentos_apps_releases SET status = 'ready', build_error = NULL
+ `UPDATE dynamic_apps_releases SET status = 'ready', build_error = NULL
WHERE release_id = ?`,
release.release,
);
@@ -786,7 +789,7 @@ async function commitReleasePublishLocked(
}
const appId = c.key[0];
if (!appId)
- fail("agentos_apps_invalid_app_id", "application actor key is missing");
+ fail("dynamic_apps_invalid_app_id", "application actor key is missing");
const runtime = await provisionAppNamespace(
appId,
resolveDefaultRivetConnection(),
@@ -800,17 +803,36 @@ async function commitReleasePublishLocked(
state.runnerToken = runtime.runnerToken ?? null;
state.publicToken = runtime.publicToken ?? null;
if (release.usesRivetKit) {
- actorPublicEndpoint(release, state);
+ const publicEndpoint = actorPublicEndpoint(release, state);
if (
runtime.endpoint.replace(/\/$/, "") !==
release.runtimeEndpoint.replace(/\/$/, "")
) {
fail(
- "agentos_apps_runtime_changed",
+ "dynamic_apps_runtime_changed",
"the app actor Rivet endpoint does not match the deployment runtime",
);
}
+ state.activatingRelease = release.release;
try {
+ const metadataResponse = await getDefaultActorRuntime().request({
+ key: `${release.release}:${release.artifactHash}`,
+ appId,
+ release: release.release,
+ loadArtifact: () => readStoredArtifact(c.db, release),
+ endpoint: publicEndpoint,
+ namespace: release.namespace,
+ pool: release.runtimePool,
+ request: new Request("http://dynamic-app.internal/api/rivet/metadata", {
+ headers: { "user-agent": "RivetEngine/prewarm" },
+ }),
+ });
+ if (!metadataResponse.ok) {
+ throw new Error(
+ `Dynamic App actor metadata prewarm returned HTTP ${metadataResponse.status}`,
+ );
+ }
+ await metadataResponse.arrayBuffer();
await configureAppNamespaceRunner(
c.actorId,
{
@@ -822,6 +844,7 @@ async function commitReleasePublishLocked(
release.callbackSecret,
);
} catch (error) {
+ state.activatingRelease = null;
c.log.error({
msg: "Dynamic App actor runner configuration failed",
release: release.release,
@@ -831,6 +854,7 @@ async function commitReleasePublishLocked(
}
}
state.activeRelease = release.release;
+ state.activatingRelease = null;
state.revision += 1;
c.broadcast("releaseActivated", {
revision: state.revision,
@@ -849,7 +873,7 @@ async function commitReleasePublishLocked(
await deleteArtifactChunksBatched(c.db, candidate.release);
await deleteReleaseFilesBatched(c.db, candidate.release);
await c.db.execute(
- "DELETE FROM agentos_apps_releases WHERE release_id = ?",
+ "DELETE FROM dynamic_apps_releases WHERE release_id = ?",
candidate.release,
);
retained -= 1;
@@ -864,7 +888,7 @@ function deploymentForRelease(
): Deployment & { appActorId: string; usesRivetKit: boolean } {
const appId = c.key[0];
if (!appId)
- fail("agentos_apps_invalid_app_id", "application actor key is missing");
+ fail("dynamic_apps_invalid_app_id", "application actor key is missing");
return {
appId,
release: release.release,
@@ -888,8 +912,9 @@ export function createAppsActors(
const callbackPath = normalizeActorCallbackPath(request);
if (!callbackPath) return new Response("Not Found", { status: 404 });
const state = c.state as AppState;
- const release = state.activeRelease
- ? await getStoredRelease(c.db, state.activeRelease)
+ const routedRelease = state.activatingRelease ?? state.activeRelease;
+ const release = routedRelease
+ ? await getStoredRelease(c.db, routedRelease)
: undefined;
if (!release || release.status !== "ready" || !release.usesRivetKit) {
return new Response("Dynamic App has no active actor registry", {
@@ -920,7 +945,7 @@ export function createAppsActors(
}
};
- const agentOSAppsApp = actor({
+ const dynamicAppsApp = actor({
options: { actionTimeout: 16 * 60_000 },
db: db({ onMigrate: migrateAppsTables }),
onRequest: forwardActorRequest,
@@ -972,7 +997,7 @@ export function createAppsActors(
const appId = c.key[0];
if (!appId || c.key.length !== 1 || input.appId !== appId) {
fail(
- "agentos_apps_app_id_mismatch",
+ "dynamic_apps_app_id_mismatch",
"deployApp appId must match the stable application actor key",
);
}
@@ -1038,7 +1063,7 @@ export function createAppsActors(
const appId = c.key[0];
if (!appId) {
fail(
- "agentos_apps_invalid_app_id",
+ "dynamic_apps_invalid_app_id",
"application actor key is missing",
);
}
@@ -1051,19 +1076,19 @@ export function createAppsActors(
release.entrypoint !== DIRECT_ENTRYPOINT
) {
fail(
- "agentos_apps_not_deployed",
+ "dynamic_apps_not_deployed",
"app has no active direct release; call app.deploy() first",
);
}
if (requestedRegion && !release.regions.includes(requestedRegion)) {
fail(
- "agentos_apps_region_not_deployed",
+ "dynamic_apps_region_not_deployed",
`app is not deployed in requested region ${requestedRegion}`,
{ requestedRegion, regions: release.regions },
);
}
const region = requestedRegion ?? release.regions[0];
- if (!region) fail("agentos_apps_no_region", "active app has no region");
+ if (!region) fail("dynamic_apps_no_region", "active app has no region");
return {
appId,
release: release.release,
@@ -1088,14 +1113,14 @@ export function createAppsActors(
release.entrypoint !== DIRECT_ENTRYPOINT
) {
fail(
- "agentos_apps_artifact_not_ready",
+ "dynamic_apps_artifact_not_ready",
`artifact for release ${releaseId} is not ready`,
);
}
const rows = await c.db.execute<{ chunks: number; bytes: number }>(
`SELECT COUNT(*) AS chunks,
COALESCE(SUM(byte_length), 0) AS bytes
- FROM agentos_apps_artifact_chunks WHERE release_id = ?`,
+ FROM dynamic_apps_artifact_chunks WHERE release_id = ?`,
releaseId,
);
const chunks = Number(rows[0]?.chunks ?? 0);
@@ -1106,7 +1131,7 @@ export function createAppsActors(
bytes !== release.artifactBytes
) {
fail(
- "agentos_apps_artifact_manifest_invalid",
+ "dynamic_apps_artifact_manifest_invalid",
`artifact ${releaseId} failed persisted manifest validation`,
);
}
@@ -1129,7 +1154,7 @@ export function createAppsActors(
index >= MAX_ARTIFACT_CHUNKS
) {
fail(
- "agentos_apps_invalid_artifact_chunk",
+ "dynamic_apps_invalid_artifact_chunk",
`artifact chunk index must be between 0 and ${MAX_ARTIFACT_CHUNKS - 1}`,
);
}
@@ -1137,7 +1162,7 @@ export function createAppsActors(
content: Uint8Array;
byte_length: number;
}>(
- `SELECT content, byte_length FROM agentos_apps_artifact_chunks
+ `SELECT content, byte_length FROM dynamic_apps_artifact_chunks
WHERE release_id = ? AND chunk_index = ?`,
releaseId,
index,
@@ -1145,7 +1170,7 @@ export function createAppsActors(
const row = rows[0];
if (!row) {
fail(
- "agentos_apps_artifact_chunk_not_found",
+ "dynamic_apps_artifact_chunk_not_found",
`artifact chunk ${index} for release ${releaseId} was not found`,
);
}
@@ -1155,7 +1180,7 @@ export function createAppsActors(
content.byteLength > ARTIFACT_CHUNK_BYTES
) {
fail(
- "agentos_apps_artifact_chunk_invalid",
+ "dynamic_apps_artifact_chunk_invalid",
`artifact chunk ${index} failed length validation`,
);
}
@@ -1172,7 +1197,7 @@ export function createAppsActors(
},
},
});
- return { agentOSAppsApp };
+ return { dynamicAppsApp };
}
/** @internal Preserves callback streaming until runtime admission. */
diff --git a/packages/dynamic-apps/src/control-plane.ts b/packages/dynamic-apps/src/control-plane.ts
index 0bda1489a..58d7555c8 100644
--- a/packages/dynamic-apps/src/control-plane.ts
+++ b/packages/dynamic-apps/src/control-plane.ts
@@ -36,7 +36,7 @@ async function readBoundedJson(response: Response): Promise {
if (bytes > MAX_CONTROL_RESPONSE_BYTES) {
await reader.cancel("Dynamic Apps control response limit exceeded");
throw new DynamicAppsError(
- "agentos_apps_control_response_limit",
+ "dynamic_apps_control_response_limit",
`Rivet control response exceeded ${MAX_CONTROL_RESPONSE_BYTES} bytes`,
{ limit: MAX_CONTROL_RESPONSE_BYTES },
);
@@ -139,7 +139,7 @@ async function provisionCloudNamespace(
const cloudToken = process.env.RIVET_CLOUD_TOKEN;
if (!cloudToken) {
throw new DynamicAppsError(
- "agentos_apps_cloud_token_required",
+ "dynamic_apps_cloud_token_required",
"RIVET_CLOUD_TOKEN is required",
);
}
@@ -150,7 +150,7 @@ async function provisionCloudNamespace(
}>(
cloudUrl("/tokens/api/inspect"),
{ headers },
- "agentos_apps_cloud_token_invalid",
+ "dynamic_apps_cloud_token_invalid",
"Rivet Cloud token inspection",
);
const displayName = appNamespaceDisplayName(appId, connection.namespace);
@@ -170,7 +170,7 @@ async function provisionCloudNamespace(
}>(
url,
{ headers },
- "agentos_apps_namespace_lookup_failed",
+ "dynamic_apps_namespace_lookup_failed",
"Rivet Cloud namespace lookup",
);
cloudNamespace = listed.namespaces.find(
@@ -195,7 +195,7 @@ async function provisionCloudNamespace(
headers,
body: JSON.stringify({ displayName }),
},
- "agentos_apps_namespace_create_failed",
+ "dynamic_apps_namespace_create_failed",
"Rivet Cloud namespace creation",
);
cloudNamespace = created.namespace.name;
@@ -207,25 +207,25 @@ async function provisionCloudNamespace(
}>(
cloudUrl(namespacePath, identity.organization),
{ headers },
- "agentos_apps_namespace_lookup_failed",
+ "dynamic_apps_namespace_lookup_failed",
"Rivet Cloud namespace resolution",
),
checkedJson<{ token: string }>(
cloudUrl(`${namespacePath}/tokens/access`, identity.organization),
{ method: "POST", headers },
- "agentos_apps_namespace_token_failed",
+ "dynamic_apps_namespace_token_failed",
"Rivet Cloud access token creation",
),
checkedJson<{ token: string }>(
cloudUrl(`${namespacePath}/tokens/secret`, identity.organization),
{ method: "POST", headers },
- "agentos_apps_namespace_token_failed",
+ "dynamic_apps_namespace_token_failed",
"Rivet Cloud runner token creation",
),
checkedJson<{ token: string }>(
cloudUrl(`${namespacePath}/tokens/publishable`, identity.organization),
{ method: "POST", headers },
- "agentos_apps_namespace_token_failed",
+ "dynamic_apps_namespace_token_failed",
"Rivet Cloud publishable token creation",
),
]);
@@ -264,7 +264,7 @@ export async function provisionAppNamespace(
});
if (!response.ok) {
throw new DynamicAppsError(
- "agentos_apps_namespace_lookup_failed",
+ "dynamic_apps_namespace_lookup_failed",
`Rivet namespace lookup failed with HTTP ${response.status}`,
{ status: response.status },
);
@@ -289,7 +289,7 @@ export async function provisionAppNamespace(
);
if (!response.ok && !(await lookup())) {
throw new DynamicAppsError(
- "agentos_apps_namespace_create_failed",
+ "dynamic_apps_namespace_create_failed",
`Rivet namespace creation failed with HTTP ${response.status}`,
{ status: response.status },
);
@@ -366,7 +366,7 @@ export async function configureAppNamespaceRunner(
});
} catch (error) {
throw new DynamicAppsError(
- "agentos_apps_runner_config_failed",
+ "dynamic_apps_runner_config_failed",
`Rivet runner configuration failed for namespace ${runtime.namespace} and pool ${runtime.pool}`,
{
namespace: runtime.namespace,
diff --git a/packages/dynamic-apps/src/deploy.ts b/packages/dynamic-apps/src/deploy.ts
index f896b52b4..13eabb5e9 100644
--- a/packages/dynamic-apps/src/deploy.ts
+++ b/packages/dynamic-apps/src/deploy.ts
@@ -24,7 +24,7 @@ interface DeploymentActorGroup {
export interface DeployAppOptions {
/** An ordinary RivetKit client. The default client is created lazily. */
client?: {
- agentOSAppsApp: DeploymentActorGroup;
+ dynamicAppsApp: DeploymentActorGroup;
};
}
@@ -46,7 +46,7 @@ export async function deployApp(
scaling: input.scaling,
};
const result = await deployThroughStableActor(
- options.client.agentOSAppsApp,
+ options.client.dynamicAppsApp,
input.appId,
prepared,
);
diff --git a/packages/dynamic-apps/src/index.ts b/packages/dynamic-apps/src/index.ts
index 70efb4ac3..ed0b55fcf 100644
--- a/packages/dynamic-apps/src/index.ts
+++ b/packages/dynamic-apps/src/index.ts
@@ -7,3 +7,4 @@ export {
setDynamicAppsLogHandler,
} from "./logging.js";
export { appsRouter } from "./router.js";
+export { rivetActorsSkill, webServerSkill } from "./skills.js";
diff --git a/packages/dynamic-apps/src/registry.ts b/packages/dynamic-apps/src/registry.ts
index 76db8c375..60524243c 100644
--- a/packages/dynamic-apps/src/registry.ts
+++ b/packages/dynamic-apps/src/registry.ts
@@ -1,10 +1,42 @@
+import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
+import { join } from "node:path";
import { setup } from "rivetkit";
import { createAppsActors } from "./actors.js";
+const artifactCacheDirectory = process.env.DYNAMIC_APPS_E2E_ARTIFACT_CACHE;
+
export const privateAppsRegistry = setup({
- use: createAppsActors() as unknown as Record<
+ // Artifact chunks are binary deployment traffic and expand during RivetKit
+ // action serialization. The default 64 KiB action limit is too small.
+ maxIncomingMessageSize: 1024 * 1024,
+ use: createAppsActors({
+ artifactCache: artifactCacheDirectory
+ ? {
+ async get(release) {
+ try {
+ return new Uint8Array(
+ await readFile(
+ join(artifactCacheDirectory, `${release}.aospkg`),
+ ),
+ );
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === "ENOENT")
+ return undefined;
+ throw error;
+ }
+ },
+ async put(release, artifact) {
+ await mkdir(artifactCacheDirectory, { recursive: true });
+ const target = join(artifactCacheDirectory, `${release}.aospkg`);
+ const temporary = `${target}.${process.pid}.tmp`;
+ await writeFile(temporary, artifact);
+ await rename(temporary, target);
+ },
+ }
+ : undefined,
+ }) as unknown as Record<
string,
- ReturnType["agentOSAppsApp"]
+ ReturnType["dynamicAppsApp"]
>,
});
diff --git a/packages/dynamic-apps/src/release-store.ts b/packages/dynamic-apps/src/release-store.ts
index eecac9edd..55076b4db 100644
--- a/packages/dynamic-apps/src/release-store.ts
+++ b/packages/dynamic-apps/src/release-store.ts
@@ -15,7 +15,9 @@ import { createClient } from "rivetkit/client";
import { ensurePrivateAppsRegistry } from "./registry.js";
import type { AppRouteResolution, Deployment } from "./types.js";
-export const ARTIFACT_CHUNK_BYTES = 512 * 1024;
+// Keep the raw payload comfortably below the Engine action-message limit after
+// Uint8Array JSON/base64 serialization and protocol framing.
+export const ARTIFACT_CHUNK_BYTES = 128 * 1024;
const MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
const MAX_ARTIFACT_CHUNKS = Math.ceil(
MAX_ARTIFACT_BYTES / ARTIFACT_CHUNK_BYTES,
@@ -100,7 +102,7 @@ interface ReleaseActorGroup {
}
interface ReleaseStoreClient {
- agentOSAppsApp: ReleaseActorGroup;
+ dynamicAppsApp: ReleaseActorGroup;
}
interface DriverEntry {
@@ -137,7 +139,7 @@ export function createRivetReleaseStore(
input: PublishReleaseInput,
): Promise => {
await ensurePrivateAppsRegistry();
- const group = getClient().agentOSAppsApp;
+ const group = getClient().dynamicAppsApp;
const driver = drivers.get(input.appId);
let handle: AppReleaseHandle;
let usedExisting = false;
@@ -221,7 +223,7 @@ export function createRivetReleaseStore(
handle.resolveDeployment(),
);
} catch (error) {
- if (getErrorCode(error) === "agentos_apps_not_deployed") return undefined;
+ if (getErrorCode(error) === "dynamic_apps_not_deployed") return undefined;
throw error;
}
const manifest = await timed(context, "artifact-manifest", () =>
@@ -291,7 +293,7 @@ export function createRivetReleaseStore(
drivers.set(appId, entry);
entry.ready = connectDriver(
entry,
- getClient().agentOSAppsApp,
+ getClient().dynamicAppsApp,
invalidate,
).catch(async (error) => {
if (drivers.get(appId) === entry) drivers.delete(appId);
diff --git a/packages/dynamic-apps/src/router.ts b/packages/dynamic-apps/src/router.ts
index cd52d0dca..af89fbe9a 100644
--- a/packages/dynamic-apps/src/router.ts
+++ b/packages/dynamic-apps/src/router.ts
@@ -1,24 +1,15 @@
-import type { Hono } from "hono";
+import { Hono } from "hono";
import type { BlankEnv, BlankSchema } from "hono/types";
import { defaultDynamicApps } from "./default.js";
import { handlePrivateAppsRegistry } from "./registry.js";
-const PRIVATE_REGISTRY_SENTINEL = "x-agentos-app-registry-dispatch";
+const router = new Hono();
-const router = defaultDynamicApps.appsRouter;
-const honoFetch = router.fetch.bind(router);
-router.fetch = (async (request: Request, ...rest: unknown[]) => {
- if (request.headers.get(PRIVATE_REGISTRY_SENTINEL) === "1") {
- const headers = new Headers(request.headers);
- headers.delete(PRIVATE_REGISTRY_SENTINEL);
- const cleanRequest = new Request(request, { headers });
- const path = new URL(cleanRequest.url).pathname.replace(/\/+$/, "") || "/";
- if (path === "/api/rivet" || path.startsWith("/api/rivet/")) {
- return handlePrivateAppsRegistry(cleanRequest);
- }
- return new Response("Not Found", { status: 404 });
- }
- return honoFetch(request, ...rest);
-}) as typeof router.fetch;
+router.all("/api/rivet/*", (context, next) => {
+ const path = new URL(context.req.raw.url).pathname;
+ if (!path.startsWith("/api/rivet/")) return next();
+ return handlePrivateAppsRegistry(context.req.raw);
+});
+router.route("/", defaultDynamicApps.appsRouter);
export const appsRouter: Hono = router;
diff --git a/packages/dynamic-apps/src/runtime.ts b/packages/dynamic-apps/src/runtime.ts
index 52056b27e..3e0aeb5c8 100644
--- a/packages/dynamic-apps/src/runtime.ts
+++ b/packages/dynamic-apps/src/runtime.ts
@@ -9,7 +9,7 @@ export const APP_CALLBACK_SECRET_HEADER = "x-agentos-app-callback-token";
/** Stable compatibility pool retained in deployApp's result. */
export function appRunnerPool(appId: string): string {
const suffix = createHash("sha256").update(appId).digest("hex").slice(0, 16);
- return `agentos-apps-${suffix}`;
+ return `dynamic-apps-${suffix}`;
}
async function readBoundedText(response: Response): Promise {
diff --git a/packages/dynamic-apps/src/skills.ts b/packages/dynamic-apps/src/skills.ts
new file mode 100644
index 000000000..db5f584a9
--- /dev/null
+++ b/packages/dynamic-apps/src/skills.ts
@@ -0,0 +1,151 @@
+/** Instructions and a complete starter for a buildable TypeScript HTTP app. */
+export const webServerSkill = `Build a Node.js web server in TypeScript. Start from this complete project and modify it for the user's request.
+
+package.json
+~~~json
+{
+ "name": "generated-web-app",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "main": "dist/index.js",
+ "scripts": {
+ "build": "tsc",
+ "start": "node dist/index.js"
+ },
+ "dependencies": {
+ "@hono/node-server": "2.0.11",
+ "hono": "4.12.9"
+ },
+ "devDependencies": {
+ "@types/node": "22.19.15",
+ "typescript": "5.7.3"
+ }
+}
+~~~
+
+tsconfig.json
+~~~json
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "outDir": "dist",
+ "types": ["node"],
+ "skipLibCheck": true
+ },
+ "include": ["src"]
+}
+~~~
+
+src/index.ts
+~~~ts
+import { serve } from "@hono/node-server";
+import { Hono } from "hono";
+
+const app = new Hono();
+
+app.get("/", (context) =>
+ context.json({ ok: true, message: "Hello from Dynamic Apps" }),
+);
+
+serve({
+ fetch: app.fetch,
+ port: Number(process.env.PORT ?? 3000),
+});
+~~~
+
+Keep the build script as "tsc" and the entrypoint as dist/index.js. The deployment runs npm run build, so invalid generated TypeScript returns compiler diagnostics that can be used to repair the files.`;
+
+/** Instructions and a complete starter for a TypeScript app with Rivet actors. */
+export const rivetActorsSkill = `Build a Node.js TypeScript web server with Rivet Actors. Start from this complete project and modify the actor state, actions, and HTTP routes for the user's request.
+
+package.json
+~~~json
+{
+ "name": "generated-rivet-actors-app",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "main": "dist/index.js",
+ "scripts": {
+ "build": "tsc",
+ "start": "node dist/index.js"
+ },
+ "dependencies": {
+ "@hono/node-server": "2.0.11",
+ "hono": "4.12.9",
+ "rivetkit": "2.3.11"
+ },
+ "devDependencies": {
+ "@types/node": "22.19.15",
+ "typescript": "5.7.3"
+ }
+}
+~~~
+
+tsconfig.json
+~~~json
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "outDir": "dist",
+ "types": ["node"],
+ "skipLibCheck": true
+ },
+ "include": ["src"]
+}
+~~~
+
+src/index.ts
+~~~ts
+import { serve } from "@hono/node-server";
+import { Hono } from "hono";
+import { actor, event, setup } from "rivetkit";
+
+const counter = actor({
+ state: { count: 0 },
+ events: { countChanged: event() },
+ actions: {
+ increment(context, amount: number = 1) {
+ context.state.count += amount;
+ context.broadcast("countChanged", context.state.count);
+ return context.state.count;
+ },
+ getCount(context) {
+ return context.state.count;
+ }
+ }
+});
+
+export const registry = setup({ use: { counter } });
+
+const app = new Hono();
+app.all("/api/rivet/*", (context) => registry.handler(context.req.raw));
+app.get("/", (context) =>
+ context.json({ ok: true, message: "Rivet Actors server is running" }),
+);
+
+serve({
+ fetch: app.fetch,
+ port: Number(process.env.PORT ?? 3000),
+});
+~~~
+
+Do not call registry.start(): the Hono server owns the HTTP listener. Keep the build script as "tsc" so deployment failures include TypeScript diagnostics. The example above works without reading external documentation.
+
+Optional reference links:
+- Rivet Actors: https://rivet.dev/actors/docs/
+- Node.js quickstart: https://rivet.dev/actors/docs/quickstart/backend/
+- Hono integration: https://rivet.dev/actors/docs/general/http-server/
+- State: https://rivet.dev/actors/docs/state/
+- Actions: https://rivet.dev/actors/docs/actions/
+- Events: https://rivet.dev/actors/docs/events/
+- SQLite: https://rivet.dev/actors/docs/sqlite/`;
diff --git a/packages/dynamic-apps/tests/direct.test.ts b/packages/dynamic-apps/tests/direct.test.ts
index 9a9b0809e..a7135750b 100644
--- a/packages/dynamic-apps/tests/direct.test.ts
+++ b/packages/dynamic-apps/tests/direct.test.ts
@@ -105,7 +105,7 @@ describe("retained public surface", () => {
const response = await appsRouter.request("/INVALID/");
expect(response.status).toBe(400);
expect(await response.json()).toMatchObject({
- error: { code: "agentos_apps_invalid_app_id" },
+ error: { code: "dynamic_apps_invalid_app_id" },
});
expect(calls).toBe(0);
});
@@ -124,7 +124,7 @@ describe("retained public surface", () => {
},
{
client: {
- agentOSAppsApp: {
+ dynamicAppsApp: {
getOrCreate(value) {
key = value;
return {
@@ -135,7 +135,7 @@ describe("retained public surface", () => {
release: "release-1",
endpoint: "https://api.example.test",
namespace: "app-demo",
- pool: "agentos-apps-demo",
+ pool: "dynamic-apps-demo",
token: "pk_demo",
regions: ["us-west"],
appActorId: "actor-1",
@@ -173,7 +173,7 @@ describe("retained public surface", () => {
},
{
client: {
- agentOSAppsApp: {
+ dynamicAppsApp: {
get() {
calls.push("get");
return {
@@ -205,7 +205,7 @@ describe("retained public surface", () => {
},
{
client: {
- agentOSAppsApp: {
+ dynamicAppsApp: {
get() {
calls.push("get");
return {
@@ -279,7 +279,7 @@ function deploymentResult(_input: unknown) {
release: "release-1",
endpoint: "https://api.example.test",
namespace: "app-demo",
- pool: "agentos-apps-demo",
+ pool: "dynamic-apps-demo",
token: "pk_demo",
regions: ["default"],
appActorId: "actor-1",
@@ -578,7 +578,7 @@ describe("direct agentOS execution", () => {
releaseFirstBody();
expect((await first).status).toBe(200);
expect(await secondError).toMatchObject({
- code: "agentos_apps_no_capacity",
+ code: "dynamic_apps_no_capacity",
});
} finally {
releaseFirstBody();
@@ -749,7 +749,7 @@ export default { fetch: () => new Response("ok") };
}
});
- test("bounds concurrent actor workers as well as idle cache entries", async () => {
+ test("does not reject active actor runtimes when the idle cache target is full", async () => {
const artifact = await makeActorArtifact(`
export default {
async fetch() {
@@ -761,7 +761,7 @@ export default {
const runtime = new DynamicActorRuntime({
DYNAMIC_APPS_ACTOR_WORKER_MAX_ENTRIES: "4",
});
- const outcomes = Array.from({ length: 16 }, (_, index) =>
+ const outcomes = Array.from({ length: 6 }, (_, index) =>
runtime
.request({
key: `bounded-${index}`,
@@ -779,15 +779,14 @@ export default {
),
);
try {
- await new Promise((resolve) => setTimeout(resolve, 50));
- const diagnostics = runtime.diagnostics();
- expect(diagnostics.entries + diagnostics.creating).toBeLessThanOrEqual(4);
+ expect(await Promise.all(outcomes)).toHaveLength(6);
+ await waitFor(() => runtime.diagnostics().entries <= 4);
} finally {
await Promise.allSettled(outcomes);
await runtime.dispose();
await artifact.dispose();
}
- }, 5_000);
+ }, 60_000);
test("times out an actor handler that blocks its worker", async () => {
const artifact = await makeActorArtifact(`
@@ -817,7 +816,7 @@ export default {
.then(
() => "response" as const,
(error: unknown) =>
- String(error).includes("request exceeded")
+ String(error).includes("response headers exceeded")
? ("timeout" as const)
: Promise.reject(error),
),
@@ -886,7 +885,9 @@ export default {
expect(pulls).toBe(1);
});
- test("preserves actor worker error stacks and causes", async () => {
+ test("logs actor server error stacks and causes", async () => {
+ const events: Readonly[] = [];
+ setDynamicAppsLogHandler((event) => events.push(event));
const artifact = await makeActorArtifact(`
export default {
async fetch() {
@@ -896,25 +897,29 @@ export default {
`);
const runtime = new DynamicActorRuntime();
try {
- await expect(
- runtime.request({
- key: "worker-error",
- loadArtifact: async () => artifact.bytes,
- endpoint: "http://example.test",
- namespace: "test",
- pool: "default",
- request: new Request("http://example.test/start", {
- method: "POST",
- }),
- }),
- ).rejects.toThrow(/outer failure[\s\S]*Caused by:[\s\S]*inner failure/u);
+ const response = await runtime.request({
+ key: "worker-error",
+ loadArtifact: async () => artifact.bytes,
+ endpoint: "http://example.test",
+ namespace: "test",
+ pool: "default",
+ request: new Request("http://example.test/start", { method: "POST" }),
+ });
+ expect(response.status).toBe(500);
+ await response.arrayBuffer();
+ await waitFor(() =>
+ events.some((event) => event.message.includes("outer failure")),
+ );
+ const messages = events.map((event) => event.message).join("\n");
+ expect(messages).toContain("outer failure");
+ expect(messages).toContain("inner failure");
} finally {
await runtime.dispose();
await artifact.dispose();
}
});
- test("admits actor callback bodies before reading them", async () => {
+ test("does not gate concurrent actor callback bodies", async () => {
const artifact = await makeActorArtifact(`
export default {
async fetch() {
@@ -922,11 +927,7 @@ export default {
},
};
`);
- const runtime = new DynamicActorRuntime({
- DYNAMIC_APPS_ACTOR_REQUEST_CONCURRENCY: "4",
- DYNAMIC_APPS_ACTOR_REQUEST_QUEUE_SIZE: "4",
- DYNAMIC_APPS_ACTOR_REQUEST_QUEUE_WAIT_MS: "1000",
- });
+ const runtime = new DynamicActorRuntime();
let release = () => {};
const gate = new Promise((resolve) => {
release = resolve;
@@ -954,16 +955,16 @@ export default {
),
);
try {
- await waitFor(() => pulls >= 4);
+ await waitFor(() => pulls >= 16);
await new Promise((resolve) => setTimeout(resolve, 20));
- expect(pulls).toBe(4);
+ expect(pulls).toBe(16);
} finally {
release();
await Promise.allSettled(outcomes);
await runtime.dispose();
await artifact.dispose();
}
- });
+ }, 60_000);
test("stops reading a callback body when it crosses the limit", async () => {
const runtime = new DynamicActorRuntime({
@@ -1052,27 +1053,21 @@ export default {
const outcome = await Promise.race([
Promise.all(
Array.from({ length: 8 }, async (_, index) => {
- try {
- const response = await runtime.request({
- key: `churn-${index}`,
- loadArtifact: async () => artifact.bytes,
- endpoint: "http://example.test",
- namespace: "test",
- pool: "default",
- request: new Request(`http://example.test/${index}`, {
- method: "POST",
- }),
- });
- expect(await response.text()).toBe("ok");
- } catch (error) {
- expect(error).toMatchObject({
- code: "agentos_apps_no_capacity",
- });
- }
+ const response = await runtime.request({
+ key: `churn-${index}`,
+ loadArtifact: async () => artifact.bytes,
+ endpoint: "http://example.test",
+ namespace: "test",
+ pool: "default",
+ request: new Request(`http://example.test/${index}`, {
+ method: "POST",
+ }),
+ });
+ expect(await response.text()).toBe("ok");
}),
).then(() => "complete" as const),
new Promise<"hung">((resolve) =>
- setTimeout(() => resolve("hung"), 1_000),
+ setTimeout(() => resolve("hung"), 30_000),
),
]);
expect(outcome).toBe("complete");
@@ -1085,7 +1080,7 @@ export default {
await runtime.dispose();
await artifact.dispose();
}
- }, 5_000);
+ }, 60_000);
test("attributes actor worker stdout and stderr", async () => {
const events: Readonly[] = [];
@@ -1249,12 +1244,47 @@ async function makeActorArtifact(source: string): Promise {
await writeFile(join(directory, "actor", "application.mjs"), source);
await writeFile(
join(directory, "actor", "main.mjs"),
- `const { default: application } = await import("./application.mjs");
+ `import http from "node:http";
+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;
+const port = Number(process.env.DYNAMIC_APPS_ACTOR_PORT ?? 3000);
+http.createServer(async (incoming, outgoing) => {
+ try {
+ if (incoming.url === "/.agentos/ready") {
+ outgoing.writeHead(200);
+ outgoing.end("ok");
+ return;
+ }
+ const chunks = await new Promise((resolve, reject) => {
+ const value = [];
+ incoming.on("data", (chunk) => value.push(Buffer.from(chunk)));
+ incoming.once("end", () => resolve(value));
+ incoming.once("error", reject);
+ });
+ const request = new Request(new URL(incoming.url ?? "/", "http://actor.test"), {
+ method: incoming.method,
+ headers: incoming.headers,
+ body: incoming.method === "GET" || incoming.method === "HEAD" ? undefined : Buffer.concat(chunks),
+ });
+ const response = await fetch(request);
+ outgoing.statusCode = response.status;
+ response.headers.forEach((value, name) => outgoing.setHeader(name, value));
+ outgoing.flushHeaders();
+ if (response.body) {
+ for await (const chunk of response.body) {
+ if (!outgoing.write(Buffer.from(chunk))) await new Promise((resolve) => outgoing.once("drain", resolve));
+ }
+ }
+ outgoing.end();
+ } catch (error) {
+ console.error(error);
+ if (!outgoing.headersSent) outgoing.writeHead(500);
+ if (!outgoing.writableEnded) outgoing.end("Internal Server Error");
+ }
+}).listen(port, "0.0.0.0");
`,
);
await writeFile(
diff --git a/packages/dynamic-apps/tests/fixtures/legacy-0.2.15.json b/packages/dynamic-apps/tests/fixtures/legacy-0.2.15.json
index 1a59108f6..ca48f36b1 100644
--- a/packages/dynamic-apps/tests/fixtures/legacy-0.2.15.json
+++ b/packages/dynamic-apps/tests/fixtures/legacy-0.2.15.json
@@ -1,18 +1,18 @@
{
"sourcePackages": {
- "@rivet-dev/agentos-apps": "0.2.15",
+ "@rivet-dev/dynamic-apps": "0.2.15",
"@agentos-software/apps-builder": "0.2.15"
},
"sourceRevision": "d7026219dec75a886a11d89857c68327a6f9b94a",
"actorKeys": [
- "agentOSAppsApp",
- "agentOSAppsScaler",
- "agentOSAppsReplica"
+ "dynamicAppsApp",
+ "dynamicAppsScaler",
+ "dynamicAppsReplica"
],
"sqliteTables": [
- "agentos_apps_releases",
- "agentos_apps_release_files",
- "agentos_apps_artifact_chunks"
+ "dynamic_apps_releases",
+ "dynamic_apps_release_files",
+ "dynamic_apps_artifact_chunks"
],
"filesBase64": {
"index.html": "PGgxPmxlZ2FjeSBmaXh0dXJlPC9oMT4K",
@@ -20,6 +20,6 @@
},
"entrypoint": "index.html",
"packagingIdentity": "apps-builder@0.2.15;manifest@1;bundle@2;esbuild-wasm@0.27.4;rivetkit-adapter@6",
- "deploymentIdentity": "{\"regions\":[\"us-west\"],\"scaling\":{\"minReplicas\":0,\"maxReplicas\":128,\"targetConcurrency\":8},\"namespace\":\"legacy-fixture\",\"runtime\":{\"endpoint\":\"https://api.rivet.dev\",\"pool\":\"agentos-apps-legacy\"},\"usesRivetKit\":false}",
+ "deploymentIdentity": "{\"regions\":[\"us-west\"],\"scaling\":{\"minReplicas\":0,\"maxReplicas\":128,\"targetConcurrency\":8},\"namespace\":\"legacy-fixture\",\"runtime\":{\"endpoint\":\"https://api.rivet.dev\",\"pool\":\"dynamic-apps-legacy\"},\"usesRivetKit\":false}",
"releaseId": "6e9dae258355860189a434505e9f0a8a89dacbdb2b8cf62423166d95611aa069"
}
diff --git a/packages/dynamic-apps/tests/release-store.test.ts b/packages/dynamic-apps/tests/release-store.test.ts
index 43c6c3e33..b7cebacba 100644
--- a/packages/dynamic-apps/tests/release-store.test.ts
+++ b/packages/dynamic-apps/tests/release-store.test.ts
@@ -50,7 +50,7 @@ describe("Rivet release store", () => {
},
};
const store = createRivetReleaseStore({
- agentOSAppsApp: {
+ dynamicAppsApp: {
get: () => handle as never,
getOrCreate: () => handle as never,
},
@@ -59,7 +59,7 @@ describe("Rivet release store", () => {
appId: "demo",
buildId: "b".repeat(64),
artifact: {
- format: "agentos-apps-direct-v2",
+ format: "dynamic-apps-direct-v2",
entrypoint: "direct-v2/main.mjs",
hash: createHash("sha256").update(bytes).digest("hex"),
bytes,
@@ -119,7 +119,7 @@ describe("Rivet release store", () => {
},
async getArtifactManifest() {
return {
- format: "agentos-apps-direct-v2",
+ format: "dynamic-apps-direct-v2",
hash,
bytes: bytes.byteLength,
chunks: 1,
@@ -131,7 +131,7 @@ describe("Rivet release store", () => {
},
};
const store = createRivetReleaseStore({
- agentOSAppsApp: {
+ dynamicAppsApp: {
get: () => handle as never,
getOrCreate: () => handle as never,
},
@@ -182,7 +182,7 @@ describe("Rivet release store", () => {
},
};
const store = createRivetReleaseStore({
- agentOSAppsApp: {
+ dynamicAppsApp: {
get: () => handle as never,
getOrCreate: () => handle as never,
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2e205d1e3..a3cbea9dc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5,7 +5,7 @@ settings:
excludeLinksFromLockfile: false
overrides:
- '@rivet-dev/agentos-core': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
+ '@rivet-dev/agentos-core': 0.2.16-rc.3
'@rivetkit/engine-cli': 2.3.11
'@rivetkit/react': 2.3.11
'@rivetkit/rivetkit-wasm': 2.3.11
@@ -34,11 +34,11 @@ importers:
specifier: ^2.0.11
version: 2.1.1(hono@4.13.3)
'@rivet-dev/agentos-core':
- specifier: 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
- version: 0.0.0-fix-core-spawn-keep-stdin-open.242e11c(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)
+ specifier: 0.2.16-rc.3
+ version: 0.2.16-rc.3(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)
'@rivet-dev/agentos-toolchain':
- specifier: 0.2.15
- version: 0.2.15
+ specifier: 0.2.16-rc.3
+ version: 0.2.16-rc.3
'@rivet-dev/dynamic-apps':
specifier: workspace:*
version: link:../../packages/dynamic-apps
@@ -140,6 +140,103 @@ importers:
specifier: ^5.7.3
version: 5.9.3
+ examples/apps-multiplayer:
+ dependencies:
+ '@hono/node-server':
+ specifier: ^2.0.11
+ version: 2.1.1(hono@4.13.3)
+ '@rivet-dev/dynamic-apps':
+ specifier: workspace:*
+ version: link:../../packages/dynamic-apps
+ hono:
+ specifier: ^4.12.9
+ version: 4.13.3
+ rivetkit:
+ specifier: 2.3.11
+ version: 2.3.11(better-sqlite3@12.11.1)(ws@8.21.3)
+ devDependencies:
+ '@types/node':
+ specifier: ^22.19.15
+ version: 22.20.1
+ tsx:
+ specifier: ^4.20.6
+ version: 4.23.12
+ typescript:
+ specifier: ^5.7.3
+ version: 5.9.3
+
+ examples/apps-sqlite:
+ dependencies:
+ '@hono/node-server':
+ specifier: ^2.0.11
+ version: 2.1.1(hono@4.13.3)
+ '@rivet-dev/dynamic-apps':
+ specifier: workspace:*
+ version: link:../../packages/dynamic-apps
+ hono:
+ specifier: ^4.12.9
+ version: 4.13.3
+ rivetkit:
+ specifier: 2.3.11
+ version: 2.3.11(better-sqlite3@12.11.1)(ws@8.21.3)
+ devDependencies:
+ '@types/node':
+ specifier: ^22.19.15
+ version: 22.20.1
+ tsx:
+ specifier: ^4.20.6
+ version: 4.23.12
+ typescript:
+ specifier: ^5.7.3
+ version: 5.9.3
+
+ examples/apps-static-website:
+ dependencies:
+ '@hono/node-server':
+ specifier: ^2.0.11
+ version: 2.1.1(hono@4.13.3)
+ '@rivet-dev/dynamic-apps':
+ specifier: workspace:*
+ version: link:../../packages/dynamic-apps
+ hono:
+ specifier: ^4.12.9
+ version: 4.13.3
+ devDependencies:
+ '@types/node':
+ specifier: ^22.19.15
+ version: 22.20.1
+ tsx:
+ specifier: ^4.20.6
+ version: 4.23.12
+ typescript:
+ specifier: ^5.7.3
+ version: 5.9.3
+
+ examples/apps-workflows:
+ dependencies:
+ '@hono/node-server':
+ specifier: ^2.0.11
+ version: 2.1.1(hono@4.13.3)
+ '@rivet-dev/dynamic-apps':
+ specifier: workspace:*
+ version: link:../../packages/dynamic-apps
+ hono:
+ specifier: ^4.12.9
+ version: 4.13.3
+ rivetkit:
+ specifier: 2.3.11
+ version: 2.3.11(better-sqlite3@12.11.1)(ws@8.21.3)
+ devDependencies:
+ '@types/node':
+ specifier: ^22.19.15
+ version: 22.20.1
+ tsx:
+ specifier: ^4.20.6
+ version: 4.23.12
+ typescript:
+ specifier: ^5.7.3
+ version: 5.9.3
+
packages/dynamic-apps:
dependencies:
'@rivet-dev/dynamic-apps-core':
@@ -153,11 +250,11 @@ importers:
version: 2.3.11(better-sqlite3@12.11.1)(ws@8.21.3)
devDependencies:
'@rivet-dev/agentos-core':
- specifier: 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
- version: 0.0.0-fix-core-spawn-keep-stdin-open.242e11c(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)
+ specifier: 0.2.16-rc.3
+ version: 0.2.16-rc.3(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)
'@rivet-dev/agentos-toolchain':
- specifier: 0.2.15
- version: 0.2.15
+ specifier: 0.2.16-rc.3
+ version: 0.2.16-rc.3
'@types/node':
specifier: ^22.19.15
version: 22.20.1
@@ -173,6 +270,9 @@ importers:
packages/dynamic-apps-builder:
dependencies:
+ '@rivetkit/rivetkit-wasm':
+ specifier: 2.3.11
+ version: 2.3.11
esbuild-wasm:
specifier: 0.27.4
version: 0.27.4
@@ -181,11 +281,11 @@ importers:
version: 2.3.11(better-sqlite3@12.11.1)(ws@8.21.3)
devDependencies:
'@rivet-dev/agentos-core':
- specifier: 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
- version: 0.0.0-fix-core-spawn-keep-stdin-open.242e11c(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)
+ specifier: 0.2.16-rc.3
+ version: 0.2.16-rc.3(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)
'@rivet-dev/agentos-toolchain':
- specifier: 0.2.15
- version: 0.2.15
+ specifier: 0.2.16-rc.3
+ version: 0.2.16-rc.3
'@types/node':
specifier: ^22.19.15
version: 22.20.1
@@ -199,17 +299,17 @@ importers:
packages/dynamic-apps-core:
dependencies:
'@agentos-software/sh':
- specifier: 0.2.15
- version: 0.2.15
+ specifier: 0.2.16-rc.3
+ version: 0.2.16-rc.3
'@agentos-software/tar':
specifier: 0.3.5
version: 0.3.5
'@rivet-dev/agentos-core':
- specifier: 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
- version: 0.0.0-fix-core-spawn-keep-stdin-open.242e11c(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)
+ specifier: 0.2.16-rc.3
+ version: 0.2.16-rc.3(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)
'@rivet-dev/agentos-toolchain':
- specifier: 0.2.15
- version: 0.2.15
+ specifier: 0.2.16-rc.3
+ version: 0.2.16-rc.3
'@rivet-dev/dynamic-apps-builder':
specifier: workspace:0.12.0-rc.2
version: link:../dynamic-apps-builder
@@ -243,8 +343,8 @@ importers:
version: 2.3.11(better-sqlite3@12.11.1)(ws@8.21.3)
devDependencies:
'@rivet-dev/agentos-toolchain':
- specifier: 0.2.15
- version: 0.2.15
+ specifier: 0.2.16-rc.3
+ version: 0.2.16-rc.3
'@rivetkit/engine-cli':
specifier: 2.3.11
version: 2.3.11
@@ -275,8 +375,8 @@ packages:
'@agentos-software/codex-cli@0.3.4':
resolution: {integrity: sha512-SAw3EOTa90dJLgEVVoE7JJIxHwticzdD9cEM9v00gpxhxC9vdLkeBva6rgWIUB9+qD1JEYBvUCyNkIpD2kT5YQ==}
- '@agentos-software/common@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-hTMKv/gtvxlcMuLIdCi1mWVNZcIDdnbltKnNH2EEYyAEcOk7OnAuY6eEclGrLrRfojEbWr4M8JBTzd3tRLtPoA==}
+ '@agentos-software/common@0.2.16-rc.3':
+ resolution: {integrity: sha512-2+8ui1qqMKj8kDvnNz27334tC84He0YViF+3g0O+/dMiF+sPvayCjqpfiqX9BqvlT2+5mWzFWSMzVV4Gg9aBLQ==}
'@agentos-software/coreutils@0.3.4':
resolution: {integrity: sha512-tGd0gQjUjHnm+5KgOBwidwUtlkKnl9biQuD7X2sZl15VOhYqUJpnyLF33h/nF3r9WtOTclSiLj/dc2xCxmAXnw==}
@@ -296,8 +396,8 @@ packages:
'@agentos-software/gzip@0.3.4':
resolution: {integrity: sha512-l7Y/Vwiwsqgna68yYwdNCnUBPGBg5zdmtjEFeG3hnDDFLe/02h17q0gUIqJmDBIAy1q9GoizqcFi7zXO2xygKg==}
- '@agentos-software/manifest@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-pW2lqsiiclos9b0ymoFYKZ/rEvL7wq+QthUeILWH6iYw3FQF+/LE2NSFjf/9ptPNmFYfP+wPO4640cz7NU+i6w==}
+ '@agentos-software/manifest@0.2.16-rc.3':
+ resolution: {integrity: sha512-gJwJ85o3K7TFDK6RQjw+cjMUauxHOMGypoR/JDiytP1nHbp/thHVaXc95U7gSEt94sy2nh6KUK+W0Wict/sspQ==}
'@agentos-software/opencode@0.2.7':
resolution: {integrity: sha512-lZspCiMgM0+kPAA08CEvyNy8lfBx463wObKpAwbYyaVvc0KfGvbAPS6VmnuZQWmaBmJJuRMtSo14nmb8XCD16g==}
@@ -310,8 +410,8 @@ packages:
'@agentos-software/sed@0.3.4':
resolution: {integrity: sha512-J10nZnZmme2SvXK5WMK2unQlOVncMQVUCS20GZB579a2gNoayLJYHGvfJ9a4+42wOHgDKL1u74byWlydCkEyOQ==}
- '@agentos-software/sh@0.2.15':
- resolution: {integrity: sha512-ksyZBJb2yiRPPAWvYGBzsauuGU9eTmR5J/xeneS+MlrKFsQol6JOC6Yhc12YwqT4vlpTypJ1i5ZMDfXKzP3aLA==}
+ '@agentos-software/sh@0.2.16-rc.3':
+ resolution: {integrity: sha512-ho2diVXYd/L9YaXVievU0/8ecD3r/kQZ5EiM5SLoIvwi7OWdZpAc/zi2AdGHV8MRbM8mSiSRM3dPFhRDGb/krA==}
'@agentos-software/tar@0.3.5':
resolution: {integrity: sha512-hSf6PY4q1luIomFSDgVHxVDDIPdAVZxdgwrQFEYH4aIE6TXX9qatVmzR36uYLWpnFGAZTR6srREGoTnmOGV4Lg==}
@@ -1286,70 +1386,64 @@ packages:
pyodide:
optional: true
- '@rivet-dev/agentos-core@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-7jqJEu+tUhi/M6qrAKhUaPaP5ZD9jv1V2/tEoaNrKg2j4duekWUpf8p1aNT8v3V7vLhMmImvmLDDLwiC+QigWw==}
+ '@rivet-dev/agentos-core@0.2.16-rc.3':
+ resolution: {integrity: sha512-EUFOUBa3YWhiZBTGhb4Vpv+tG6978KW2I+2JFWqB3fjSbG/82MYhbjxfxoCc3idQEGfCKtm9IOQSzpdb6Q6ufw==}
- '@rivet-dev/agentos-runtime-core@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-7QVeEvIoP7Q7hkzeetw9Hl7qeZOjVREWf425OTyX4AwSK44FbUq7kzu8wfH5IJHJ8ha2kcOsazKxNbP6eBnYuQ==}
+ '@rivet-dev/agentos-runtime-core@0.2.16-rc.3':
+ resolution: {integrity: sha512-4n4w5dCmOTcy4UhIYEy2xCZFXlBtYFeh2CA5B0WjB1QGs2cFcIB3Jpg7FAz6wBO5GX968/tsUGJ1Ut5J8L3fKw==}
- '@rivet-dev/agentos-runtime-sidecar-darwin-arm64@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-L8NN5RQA/8VZB406gw9uyWUAakNAF15YMpoO2ssX+FabXavEdja77XvRYcRSq4bBPO+tEad7IOJWfBZcmEeoOw==}
+ '@rivet-dev/agentos-runtime-sidecar-darwin-arm64@0.2.16-rc.3':
+ resolution: {integrity: sha512-fy0IpWgMwdJujwqNAY6Y+AhYw4b/+4v+0CYxeeUF1peCrIEPCPlaF2V6Xpc5ljcW8mpxiVumbvU8UxbkwsJolw==}
engines: {node: '>=20'}
cpu: [arm64]
os: [darwin]
- '@rivet-dev/agentos-runtime-sidecar-darwin-x64@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-iSB0oRLMcPuuHca2y5e1vEVq02IRXYQ5R7A189F/Hj7v0UUFUnyRRLfnanc/0RPs9Zxiqq8nf/7WhfoWU1aJEQ==}
+ '@rivet-dev/agentos-runtime-sidecar-darwin-x64@0.2.16-rc.3':
+ resolution: {integrity: sha512-NiOyfMBaIqXqrg7rhdcp9wK+iWCEW9oQob0R/r4iF9Kit2uyXP2GZBFXQ4g+T6K7KTr9wEG33+fF1mLNA9a7rA==}
engines: {node: '>=20'}
cpu: [x64]
os: [darwin]
- '@rivet-dev/agentos-runtime-sidecar-linux-arm64-gnu@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-wrc8yS1bfpMowIeG8Ge9FDNy/BuXuSGnarW4cqsrSThzAIJVy4HcK3itH+lAXOgDxgJnUqYjCFk5Xk1l3aATtA==}
+ '@rivet-dev/agentos-runtime-sidecar-linux-arm64-gnu@0.2.16-rc.3':
+ resolution: {integrity: sha512-hTIkuc12FmjetKDAkuYrr3NGsjuLMeA1docsD0jxUxrfvT3APX1rLqUORfqZpnykqzEnbUtQJMu8zP3QTDvHoA==}
engines: {node: '>=20'}
cpu: [arm64]
os: [linux]
- '@rivet-dev/agentos-runtime-sidecar-linux-x64-gnu@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-vUGoVLr2mQXsrN6R+jASBvc4px/D7FR51dxY/vrBXAdlZRsIqCu8t6Mu5JPt/DiySJxvEawF8pMvhRlUWzg+gA==}
- engines: {node: '>=20'}
- cpu: [x64]
- os: [linux]
-
- '@rivet-dev/agentos-runtime-sidecar@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-3eXrQlDQgIfcbmcM1qvVmpNdA8Bew9FZ60kiFAwNhWd4KM5Tc4NmkZ19g3sVwjD35MpbU4pLjwhrpCDTzDoe0g==}
+ '@rivet-dev/agentos-runtime-sidecar@0.2.16-rc.3':
+ resolution: {integrity: sha512-0IQs0gOY8Y48im3lCTFI06UJ0RZDlFeFfe/MTiTQg1O7WWqRXMZe/oJdX4AD6vLJQLPuoI8rGSgOAMJYWngwMg==}
engines: {node: '>=20'}
- '@rivet-dev/agentos-sidecar-darwin-arm64@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-2vbHXKR2+Zk/eXI4/kzZqP2ts6bcyyTl4LLnJuU2U1DzIAAJC35uqZu3v5V4moTWciweXHnwiPtSzf+QMQlvrw==}
+ '@rivet-dev/agentos-sidecar-darwin-arm64@0.2.16-rc.3':
+ resolution: {integrity: sha512-wlnQP+Pu+DfEtb/cHda4mYyR/dKqX600kP0JILjh3rYKmhH3wWr/e/+bkLNMpTH2MrjcLNjxSUNW0Wwt1MGddg==}
engines: {node: '>=20'}
cpu: [arm64]
os: [darwin]
- '@rivet-dev/agentos-sidecar-darwin-x64@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-+Reu0fWkO/2D/2lqTEutX52Cz3Hv+x9PTGu5ZM6z832529/SIXTgl0BFykPpTbPDJhVpFBS7piId/FQXpQmphQ==}
+ '@rivet-dev/agentos-sidecar-darwin-x64@0.2.16-rc.3':
+ resolution: {integrity: sha512-UV9Nmu6ziHXQEuXMioeCZyc1emzVETwTZwF5J64O4vp/YbmGfu6TPKnEnCRUs3XyqOoh5cPRJvo0WRSzg1GtgA==}
engines: {node: '>=20'}
cpu: [x64]
os: [darwin]
- '@rivet-dev/agentos-sidecar-linux-arm64-gnu@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-majX/SiINTMpqp+URlWBJohvxO4QVHOKbQulHlpVsRVZtRg1/zymuQJY+KdWEGCvYbmW0mvmrLUU8vu7Uk8L2Q==}
+ '@rivet-dev/agentos-sidecar-linux-arm64-gnu@0.2.16-rc.3':
+ resolution: {integrity: sha512-D6fBNJz+i6cnm/AAx9pUo5m+uIKjkGP5Cl/ltU86VKsva49zXDH8f+GbnYr5OCQb+yAdoRgRHAnlrdsC1f6IVw==}
engines: {node: '>=20'}
cpu: [arm64]
os: [linux]
- '@rivet-dev/agentos-sidecar-linux-x64-gnu@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-EIQUdVPLIEizhQa4sRBA3maXvEUomBV23dPfSaV21VFTcVD6uF9HrM/fzwPZQ5Eklksv0+EgY8+fuA1opZropg==}
+ '@rivet-dev/agentos-sidecar-linux-x64-gnu@0.2.16-rc.3':
+ resolution: {integrity: sha512-82bvLR2mj7AAaCexfkyWkUmnW3jfplfM1NgFZV3PMXs5tlwuK1SrkX+Js70OZKmc45vwMUZJha3cgxOxelBTgQ==}
engines: {node: '>=20'}
cpu: [x64]
os: [linux]
- '@rivet-dev/agentos-sidecar@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- resolution: {integrity: sha512-MCbw4zTgpjCwXP543KtHJG8N/yhe2UvmpE5UV7SjoJGwpC55Ln5Z7J6BhSfK83dShvPr0IhemZ5djWa3fpQW5g==}
+ '@rivet-dev/agentos-sidecar@0.2.16-rc.3':
+ resolution: {integrity: sha512-yJshB14bF938+MyB2dsa8wXxr+xKc+9t8CDQsnMZUi956RMjEJsPsqEsf62sN93o/KLBj9itgxNkafZQygQ2qA==}
engines: {node: '>=20'}
- '@rivet-dev/agentos-toolchain@0.2.15':
- resolution: {integrity: sha512-DcjNMIvXijTNGolTVee+9+rf/G//4P7QK+IssTET8cmYqByDBl2/XDSsocRhnu1giFIxSJTFw5AwxtxO7hiAtw==}
+ '@rivet-dev/agentos-toolchain@0.2.16-rc.3':
+ resolution: {integrity: sha512-ZtL3NhK/4QGmkOoudWl21+Kzn2wFlyV6VkS2NqGBzbtGnJweWoAhFhD9eq3dZ0HvqL0WBn2r7Dy/2PX7Ki0Q5w==}
hasBin: true
'@rivetkit/bare-ts@0.6.2':
@@ -2591,10 +2685,6 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
- isolated-vm@6.2.0:
- resolution: {integrity: sha512-UuSlxSHWt2QuJ5WvBhzlIJx2VVZN/a44SqBbEZFKNdvuSyhOvhmyDo8SQ+njVbhnh/njoL/aW0bUTiFYlpweGQ==}
- engines: {node: '>=22.0.0'}
-
isomorphic-timers-promises@1.0.1:
resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==}
engines: {node: '>=10'}
@@ -2780,10 +2870,6 @@ packages:
resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==}
hasBin: true
- node-gyp-build@4.8.4:
- resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
- hasBin: true
-
node-stdlib-browser@1.3.1:
resolution: {integrity: sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==}
engines: {node: '>=10'}
@@ -3620,7 +3706,7 @@ snapshots:
'@agentos-software/codex-cli@0.3.4': {}
- '@agentos-software/common@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
+ '@agentos-software/common@0.2.16-rc.3':
dependencies:
'@agentos-software/coreutils': 0.3.4
'@agentos-software/diffutils': 0.3.4
@@ -3643,7 +3729,7 @@ snapshots:
'@agentos-software/gzip@0.3.4': {}
- '@agentos-software/manifest@0.0.0-fix-core-spawn-keep-stdin-open.242e11c': {}
+ '@agentos-software/manifest@0.2.16-rc.3': {}
'@agentos-software/opencode@0.2.7': {}
@@ -3662,7 +3748,7 @@ snapshots:
'@agentos-software/sed@0.3.4': {}
- '@agentos-software/sh@0.2.15': {}
+ '@agentos-software/sh@0.2.16-rc.3': {}
'@agentos-software/tar@0.3.5': {}
@@ -4532,24 +4618,23 @@ snapshots:
dependencies:
'@secure-exec/core': 0.2.1
- '@rivet-dev/agentos-core@0.0.0-fix-core-spawn-keep-stdin-open.242e11c(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)':
+ '@rivet-dev/agentos-core@0.2.16-rc.3(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)':
dependencies:
'@agentclientprotocol/sdk': 0.16.1(zod@4.4.3)
'@agentos-software/claude-code': 0.2.7
'@agentos-software/codex-cli': 0.3.4
- '@agentos-software/common': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
- '@agentos-software/manifest': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
+ '@agentos-software/common': 0.2.16-rc.3
+ '@agentos-software/manifest': 0.2.16-rc.3
'@agentos-software/opencode': 0.2.7
'@agentos-software/pi': 0.2.7(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(ws@8.21.3)(zod@4.4.3)
'@aws-sdk/client-s3': 3.1113.0
- '@rivet-dev/agentos-runtime-core': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
- '@rivet-dev/agentos-sidecar': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
+ '@rivet-dev/agentos-runtime-core': 0.2.16-rc.3
+ '@rivet-dev/agentos-sidecar': 0.2.16-rc.3
'@rivetkit/bare-ts': 0.6.2
'@xterm/headless': 6.0.0
better-sqlite3: 12.11.1
croner: 10.0.1
googleapis: 144.0.0
- isolated-vm: 6.2.0
long-timeout: 0.1.1
minimatch: 10.2.6
zod: 4.4.3
@@ -4563,51 +4648,47 @@ snapshots:
- utf-8-validate
- ws
- '@rivet-dev/agentos-runtime-core@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
+ '@rivet-dev/agentos-runtime-core@0.2.16-rc.3':
dependencies:
- '@rivet-dev/agentos-runtime-sidecar': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
+ '@rivet-dev/agentos-runtime-sidecar': 0.2.16-rc.3
'@rivetkit/bare-ts': 0.6.2
zod: 4.4.3
- '@rivet-dev/agentos-runtime-sidecar-darwin-arm64@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
- optional: true
-
- '@rivet-dev/agentos-runtime-sidecar-darwin-x64@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
+ '@rivet-dev/agentos-runtime-sidecar-darwin-arm64@0.2.16-rc.3':
optional: true
- '@rivet-dev/agentos-runtime-sidecar-linux-arm64-gnu@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
+ '@rivet-dev/agentos-runtime-sidecar-darwin-x64@0.2.16-rc.3':
optional: true
- '@rivet-dev/agentos-runtime-sidecar-linux-x64-gnu@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
+ '@rivet-dev/agentos-runtime-sidecar-linux-arm64-gnu@0.2.16-rc.3':
optional: true
- '@rivet-dev/agentos-runtime-sidecar@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
+ '@rivet-dev/agentos-runtime-sidecar@0.2.16-rc.3':
optionalDependencies:
- '@rivet-dev/agentos-runtime-sidecar-darwin-arm64': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
- '@rivet-dev/agentos-runtime-sidecar-darwin-x64': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
- '@rivet-dev/agentos-runtime-sidecar-linux-arm64-gnu': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
- '@rivet-dev/agentos-runtime-sidecar-linux-x64-gnu': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
+ '@rivet-dev/agentos-runtime-sidecar-darwin-arm64': 0.2.16-rc.3
+ '@rivet-dev/agentos-runtime-sidecar-darwin-x64': 0.2.16-rc.3
+ '@rivet-dev/agentos-runtime-sidecar-linux-arm64-gnu': 0.2.16-rc.3
- '@rivet-dev/agentos-sidecar-darwin-arm64@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
+ '@rivet-dev/agentos-sidecar-darwin-arm64@0.2.16-rc.3':
optional: true
- '@rivet-dev/agentos-sidecar-darwin-x64@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
+ '@rivet-dev/agentos-sidecar-darwin-x64@0.2.16-rc.3':
optional: true
- '@rivet-dev/agentos-sidecar-linux-arm64-gnu@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
+ '@rivet-dev/agentos-sidecar-linux-arm64-gnu@0.2.16-rc.3':
optional: true
- '@rivet-dev/agentos-sidecar-linux-x64-gnu@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
+ '@rivet-dev/agentos-sidecar-linux-x64-gnu@0.2.16-rc.3':
optional: true
- '@rivet-dev/agentos-sidecar@0.0.0-fix-core-spawn-keep-stdin-open.242e11c':
+ '@rivet-dev/agentos-sidecar@0.2.16-rc.3':
optionalDependencies:
- '@rivet-dev/agentos-sidecar-darwin-arm64': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
- '@rivet-dev/agentos-sidecar-darwin-x64': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
- '@rivet-dev/agentos-sidecar-linux-arm64-gnu': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
- '@rivet-dev/agentos-sidecar-linux-x64-gnu': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
+ '@rivet-dev/agentos-sidecar-darwin-arm64': 0.2.16-rc.3
+ '@rivet-dev/agentos-sidecar-darwin-x64': 0.2.16-rc.3
+ '@rivet-dev/agentos-sidecar-linux-arm64-gnu': 0.2.16-rc.3
+ '@rivet-dev/agentos-sidecar-linux-x64-gnu': 0.2.16-rc.3
- '@rivet-dev/agentos-toolchain@0.2.15':
+ '@rivet-dev/agentos-toolchain@0.2.16-rc.3':
dependencies:
'@rivetkit/bare-ts': 0.6.2
@@ -5889,10 +5970,6 @@ snapshots:
isexe@2.0.0: {}
- isolated-vm@6.2.0:
- dependencies:
- node-gyp-build: 4.8.4
-
isomorphic-timers-promises@1.0.1: {}
jose@6.2.9: {}
@@ -6043,8 +6120,6 @@ snapshots:
detect-libc: 2.1.2
optional: true
- node-gyp-build@4.8.4: {}
-
node-stdlib-browser@1.3.1:
dependencies:
assert: 2.1.0
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 4ca301ace..32c71d9ba 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -11,7 +11,7 @@ onlyBuiltDependencies:
- esbuild
overrides:
- '@rivet-dev/agentos-core': 0.0.0-fix-core-spawn-keep-stdin-open.242e11c
+ '@rivet-dev/agentos-core': 0.2.16-rc.3
'@rivetkit/engine-cli': 2.3.11
'@rivetkit/react': 2.3.11
'@rivetkit/rivetkit-wasm': 2.3.11
diff --git a/scripts/check-boundaries.mjs b/scripts/check-boundaries.mjs
index 6ba0e7bcb..f594d7b56 100644
--- a/scripts/check-boundaries.mjs
+++ b/scripts/check-boundaries.mjs
@@ -98,8 +98,8 @@ for (const path of await walk("examples/")) {
const actors = await read("packages/dynamic-apps/src/actors.ts");
for (const identity of [
- "agentOSAppsApp: AnyActorDefinition;",
- "const agentOSAppsApp = actor({",
+ "dynamicAppsApp: AnyActorDefinition;",
+ "const dynamicAppsApp = actor({",
"beginReleasePublish:",
"commitReleasePublish:",
]) {
@@ -112,7 +112,7 @@ for (const identity of [
const runtime = await read("packages/dynamic-apps-core/src/runtime.ts");
assert(
runtime.includes(
- 'hash.update("agentos-apps-release-v19-mounted-hono-router\\0")',
+ 'hash.update("dynamic-apps-release-v19-mounted-hono-router\\0")',
),
"release hash domain changed",
);
diff --git a/scripts/resolve-release.mjs b/scripts/resolve-release.mjs
index e0b7bd6aa..49a6e74d5 100644
--- a/scripts/resolve-release.mjs
+++ b/scripts/resolve-release.mjs
@@ -13,7 +13,7 @@ const args = Object.fromEntries(
let version = args.version;
if (version === "legacy") {
const [{ stdout: main }, { stdout: builder }] = await Promise.all([
- execFileAsync("npm", ["view", "@rivet-dev/agentos-apps@latest", "version"]),
+ execFileAsync("npm", ["view", "@rivet-dev/dynamic-apps@latest", "version"]),
execFileAsync("npm", [
"view",
"@agentos-software/apps-builder@latest",
diff --git a/specs/agentos-inline-runtime-and-logging.md b/specs/agentos-inline-runtime-and-logging.md
index b4d6d733b..af8597232 100644
--- a/specs/agentos-inline-runtime-and-logging.md
+++ b/specs/agentos-inline-runtime-and-logging.md
@@ -11,7 +11,7 @@ through the agentOS library's headless JavaScript API in the Compute process;
they must not start an execution actor, guest process, HTTP listener, or nested
Node server.
-Keep the existing deployment implementation and durable `agentOSAppsApp`
+Keep the existing deployment implementation and durable `dynamicAppsApp`
state actor. It remains responsible for building releases in an agentOS build
VM, persisting release artifacts in actor SQLite, activation, rollback, and
release invalidation events.
@@ -21,7 +21,7 @@ application, actor, build, and runtime logs to stdout or its logging provider.
```text
deployApp
- -> agentOSAppsApp[appId]
+ -> dynamicAppsApp[appId]
-> existing agentOS build VM
-> immutable AOSP artifact in actor SQLite
@@ -42,15 +42,17 @@ warm direct request
app-defined actor request
-> Rivet gateway
-> authenticated Dynamic Apps callback
- -> existing bounded Node worker
- -> real RivetKit WASM registry
+ -> cached AgentOS VM and guest process
+ -> real RivetKit WASM serverless registry
+ -> agentOS native VM HTTP stream
```
-The app-defined actor worker remains in this revision. It does not use
-`isolated-vm`, and its streaming response protocol is required for RivetKit
-connections and events. Replacing that worker with one-shot agentOS
-`evaluate()` would regress streaming semantics. The worker must, however,
-forward stdout and stderr through the new log hook.
+App-defined actors use agentOS as the mandatory hostile-code boundary. The app
+listens on the supplied `PORT`, and Dynamic Apps forwards the real response head
+and chunks with agentOS's native VM HTTP stream. The init packet is not mocked:
+Engine uses its encoded runner ID and protocol version. Actor traffic travels
+over RivetKit's outbound WebSocket; the SSE stream carries init, keepalive, and
+connection lifetime only.
## Non-goals
@@ -284,11 +286,11 @@ 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
- 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.
+ runtime. The app mounts `registry.handler()` in its exported fetch router. In
+ serverless mode it calls `serve()` on `PORT` and awaits the listening
+ callback; Dynamic Apps starts the actor entrypoint as a normal Node program.
+- Build the separate actor entrypoint with RivetKit and its WASM asset bundled
+ for execution inside AgentOS.
- Continue validating both bundles before activating a release.
The direct dispatcher should be an exported async function in
@@ -312,10 +314,11 @@ Include `appId`, release, request ID, and stream.
### App-defined actors
-Create actor workers with `stdout: true` and `stderr: true`, consume both Node
-streams, and use the same bounded line decoder. Add `appId` and release to
-`ActorRuntimeRequest` so output can be attributed. Emit with
-`source: "actor"`. Keep worker transport errors as runtime error events.
+Consume the AgentOS guest process stdout and stderr with the same bounded line
+decoder. There is no Dynamic Apps actor RPC framing: HTTP requests and response
+bodies use agentOS's native VM HTTP stream. Add `appId` and release to
+`ActorRuntimeRequest` so output can be attributed. Emit application output with
+`source: "actor"` and transport failures as runtime errors.
### Build and host runtime
@@ -372,7 +375,7 @@ The file must contain no import from `isolated-vm`, no custom `Request` or
Implement the public event types, the process-global handler setter, frozen
event construction, message/metadata bounds, handler-error isolation, and the
-incremental UTF-8 line decoder shared by direct executions and actor workers.
+incremental UTF-8 line decoder shared by direct executions and actor guests.
#### `packages/dynamic-apps/src/index.ts`
@@ -381,12 +384,15 @@ alongside the unchanged `appsRouter` and `deployApp` exports.
#### `packages/dynamic-apps/src/actor-runtime.ts`
-- retain the bounded Node worker and real RivetKit WASM runtime;
+- mount the verified release in AgentOS and start the bundled RivetKit WASM
+ serverless entrypoint as a guest process;
- add `appId` and release attribution to `ActorRuntimeRequest`;
-- opt into worker stdout/stderr streams and forward them through the shared
- line decoder and log emitter;
-- emit bounded worker startup, exit, timeout, and transport errors; and
-- dispose stream listeners with each worker entry.
+- wait for the guest entrypoint to report that its HTTP listening callback
+ completed, then forward every callback route through agentOS's native VM HTTP
+ stream; preserve the real `/start` SSE stream, cancellation, and backpressure;
+- treat the retained-runtime count as an idle-cache target rather than callback
+ admission, with no Dynamic Apps concurrency queue; and
+- dispose the AgentOS VM and mounted artifact with each runtime entry.
#### `packages/dynamic-apps/src/actors.ts`
@@ -432,8 +438,7 @@ native artifacts disappear from the lockfile.
- add an explicit `directAgentOs` build mode using `platform: "node"`, ESM,
and the existing bounded filesystem plugin;
- bundle real RivetKit plus its WASM asset for actor-enabled direct bundles;
-- continue keeping RivetKit external only for the platform-linked actor bundle;
- and
+- bundle RivetKit WASM in both direct and actor AgentOS bundles; and
- retain native-addon rejection and output limits.
#### `packages/dynamic-apps-builder/test/builder.test.ts`
@@ -485,8 +490,8 @@ network permission boundary. Keep native addons unsupported.
Add a brief public page titled **Collecting logs**:
-1. Explain that application `console.log`/stdout, `console.error`/stderr, actor
- worker output, and host runtime summaries become structured events.
+1. Explain that application `console.log`/stdout, `console.error`/stderr,
+ sandboxed actor output, and host runtime summaries become structured events.
2. Show `setDynamicAppsLogHandler((event) =>
process.stdout.write(JSON.stringify(event) + "\\n"))` as the recommended
Cloud Run/Rivet Compute aggregation path.
diff --git a/specs/dynamic-apps-core.md b/specs/dynamic-apps-core.md
index 5d239ab0f..256afacb9 100644
--- a/specs/dynamic-apps-core.md
+++ b/specs/dynamic-apps-core.md
@@ -141,7 +141,7 @@ export type DeployAppInput =
});
export interface ReleaseArtifact {
- format: "agentos-apps-direct-v2";
+ format: "dynamic-apps-direct-v2";
entrypoint: "direct-v2/main.mjs";
hash: string;
bytes: Uint8Array;
@@ -528,7 +528,7 @@ Implement `createDynamicApps` with no module-level mutable state:
8. `diagnostics()` delegates to the executor and adds no credentials or
artifact contents.
9. `dispose()` is idempotent and delegates to the executor. After disposal,
- deploy and request operations fail with `agentos_apps_executor_disposed`.
+ deploy and request operations fail with `dynamic_apps_executor_disposed`.
The factory keeps an instance-local disposed flag and set of in-flight deploy
promises. Check the flag before source preparation and again after build but
@@ -673,9 +673,9 @@ export function createAppsRouter(
```
Move the existing handler body unchanged except for calling the injected
-executor. Remove `PRIVATE_REGISTRY_SENTINEL`, `handlePrivateAppsRegistry`,
-`requestOverride`, `setRouterRequestOverride`, and the custom `router.fetch`
-override. Those belong only to the Rivet wrapper.
+executor. Remove private-registry dispatch, `handlePrivateAppsRegistry`,
+`requestOverride`, `setRouterRequestOverride`, and any custom `router.fetch`
+override. Registry routing belongs only to the Rivet wrapper.
Unit tests inject an `AppRequestExecutor`; do not add a new global test seam.
@@ -874,7 +874,7 @@ Do not cache artifacts in `release-store.ts`; core owns caching.
### Actor persistence protocol
-Keep `agentOSAppsApp` and all current tables. Extend `AppState` with optional
+Keep `dynamicAppsApp` and all current tables. Extend `AppState` with optional
fields so old serialized state needs no migration:
```ts
@@ -931,7 +931,7 @@ after a newer `beginReleasePublish` has superseded it.
Under the existing per-actor `serialized` lock:
1. Require `sequence === latestPublishSequence`. Otherwise throw
- `agentos_apps_publish_superseded` and keep the current active release.
+ `dynamic_apps_publish_superseded` and keep the current active release.
2. Load the building/ready row and verify expected chunk count and total bytes.
3. Stream/read all stored chunks in order and compute SHA-256; compare it to the
expected hash. Do not mark ready on mismatch.
@@ -976,7 +976,7 @@ The top-level adapter `deployApp` behaves as follows:
- With no injected `options.client`, delegate to the default core instance so
the build occurs before the release-store upload.
- With `options.client`, retain the current `prepareSource` plus
- `client.agentOSAppsApp.get/getOrCreate(...).deploy(preparedInput)` path exactly.
+ `client.dynamicAppsApp.get/getOrCreate(...).deploy(preparedInput)` path exactly.
This preserves existing injected-client behavior while making the ordinary
path use the new core architecture. Add a code comment explaining why the two
@@ -1007,11 +1007,16 @@ The core router does not know about the private Rivet registry. In adapter
`router.ts`:
1. Obtain `defaultDynamicApps.appsRouter`.
-2. Preserve the existing custom `fetch` behavior for the private sentinel
- header and `/api/rivet` paths.
+2. Register an ordinary `/api/rivet/*` Hono route that forwards to the private
+ registry handler only when the raw request path is rooted at `/api/rivet/`.
+ This keeps mounting the router at `/apps` from intercepting an app named
+ `api` whose own path begins with `/rivet/`.
3. Delegate all ordinary app traffic to the core router.
4. Export the wrapped value as `appsRouter` with the current exact Hono type.
+Do not add a bare `/api/rivet` route, a sentinel header, or a custom `fetch`
+override.
+
Do not restore `setRouterRequestOverride`; adapter tests should instantiate a
core factory with fake hooks or exercise the wrapped router directly.
diff --git a/tests/e2e/dynamic-apps/package.json b/tests/e2e/dynamic-apps/package.json
index 6eb28d8e3..eac6ce406 100644
--- a/tests/e2e/dynamic-apps/package.json
+++ b/tests/e2e/dynamic-apps/package.json
@@ -4,9 +4,9 @@
"private": true,
"type": "module",
"scripts": {
- "test:e2e": "node --import tsx src/run.ts",
- "test:e2e:build": "node --import tsx src/run.ts --build-only",
- "test:e2e:fast": "node --import tsx src/run.ts --fast",
+ "test:e2e": "npx tsx src/run.ts",
+ "test:e2e:build": "npx tsx src/run.ts --build-only",
+ "test:e2e:fast": "npx tsx src/run.ts --fast",
"check-types": "tsc --noEmit"
},
"dependencies": {
@@ -15,7 +15,7 @@
"rivetkit": "2.3.11"
},
"devDependencies": {
- "@rivet-dev/agentos-toolchain": "0.2.15",
+ "@rivet-dev/agentos-toolchain": "0.2.16-rc.3",
"@rivetkit/engine-cli": "2.3.11",
"@types/node": "^22.19.15",
"get-port": "^7.1.0",
diff --git a/tests/e2e/dynamic-apps/src/run.ts b/tests/e2e/dynamic-apps/src/run.ts
index 1505371b3..cb1449a4f 100644
--- a/tests/e2e/dynamic-apps/src/run.ts
+++ b/tests/e2e/dynamic-apps/src/run.ts
@@ -8,10 +8,14 @@ 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 root = await mkdtemp(join(tmpdir(), "dynamic-apps-e2e-"));
const databasePath = join(root, "db");
await mkdir(databasePath, { recursive: true });
-const guardPort = await getPort();
+const configuredGuardPort = Number(process.env.DYNAMIC_APPS_E2E_ENGINE_PORT);
+const guardPort =
+ Number.isInteger(configuredGuardPort) && configuredGuardPort > 0
+ ? configuredGuardPort
+ : await getPort();
const peerPort = await getPort({ exclude: [guardPort] });
const metricsPort = await getPort({ exclude: [guardPort, peerPort] });
const endpoint = `http://127.0.0.1:${guardPort}`;
@@ -48,15 +52,15 @@ const engine = spawn(getEnginePath(), ["--config", configPath, "start"], {
try {
await waitUntilHealthy(endpoint, engine);
process.env.RIVET_ENDPOINT = endpoint;
- process.env.AGENTOS_APPS_E2E_FAST = fast ? "1" : "0";
- process.env.AGENTOS_APPS_E2E_BUILD_ONLY = buildOnly ? "1" : "0";
+ process.env.DYNAMIC_APPS_E2E_FAST = fast ? "1" : "0";
+ process.env.DYNAMIC_APPS_E2E_BUILD_ONLY = buildOnly ? "1" : "0";
if (fast) {
- process.env.AGENTOS_APPS_E2E_ARTIFACT_CACHE = join(
+ process.env.DYNAMIC_APPS_E2E_ARTIFACT_CACHE = join(
tmpdir(),
- "agentos-apps-artifact-cache-v16",
+ "dynamic-apps-artifact-cache-v1",
);
} else {
- delete process.env.AGENTOS_APPS_E2E_ARTIFACT_CACHE;
+ delete process.env.DYNAMIC_APPS_E2E_ARTIFACT_CACHE;
}
delete process.env.RIVET_ENGINE;
delete process.env.RIVET_RUN_ENGINE;
diff --git a/tests/e2e/dynamic-apps/src/sanity.ts b/tests/e2e/dynamic-apps/src/sanity.ts
index fcba20eb8..1e3ef8172 100644
--- a/tests/e2e/dynamic-apps/src/sanity.ts
+++ b/tests/e2e/dynamic-apps/src/sanity.ts
@@ -1,7 +1,17 @@
-import { appsRouter, deployApp } from "@rivet-dev/dynamic-apps";
+import {
+ appsRouter,
+ deployApp,
+ setDynamicAppsLogHandler,
+} from "@rivet-dev/dynamic-apps";
import { createClient } from "rivetkit/client";
-const appId = `dynamic-apps-sanity-${Date.now()}`;
+setDynamicAppsLogHandler((event) => {
+ console.error(
+ `[dynamic-apps:${event.source}:${event.stream ?? event.level}] ${event.message}`,
+ );
+});
+
+const appId = "dynamic-apps-sanity";
const deployment = await deployApp({
appId,
files: {
@@ -11,10 +21,15 @@ const deployment = await deployApp({
private: true,
type: "module",
main: "index.js",
- dependencies: { hono: "4.13.3", rivetkit: "2.3.11" },
+ dependencies: {
+ "@hono/node-server": "2.1.1",
+ hono: "4.13.3",
+ rivetkit: "2.3.11",
+ },
}),
"index.js": `
import { Hono } from "hono";
+import { serve } from "@hono/node-server";
import { actor, setup } from "rivetkit";
import { db } from "rivetkit/db";
const counter = actor({
@@ -33,6 +48,15 @@ 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" }));
+if (process.env.RIVETKIT_RUNTIME_MODE === "serverless") {
+ await new Promise((resolve, reject) => {
+ const server = serve(
+ { fetch: app.fetch, port: Number(process.env.PORT), hostname: "0.0.0.0" },
+ resolve,
+ );
+ server.once("error", reject);
+ });
+}
export default app;
`,
},
@@ -63,6 +87,8 @@ console.log(
event: "dynamic_apps_sanity_passed",
deployment,
direct: true,
+ health: true,
+ metadata: true,
sqlite: [first, second],
},
null,
diff --git a/tests/e2e/dynamic-apps/src/verify.ts b/tests/e2e/dynamic-apps/src/verify.ts
index a38a229cc..8ceb0b62e 100644
--- a/tests/e2e/dynamic-apps/src/verify.ts
+++ b/tests/e2e/dynamic-apps/src/verify.ts
@@ -149,12 +149,14 @@ async function deployActorFixture() {
type: "module",
main: "index.js",
dependencies: {
+ "@hono/node-server": "2.1.1",
hono: "4.13.3",
rivetkit: "2.3.11",
},
}),
"index.js": `
import { Hono } from "hono";
+import { serve } from "@hono/node-server";
import { actor, event, setup } from "rivetkit";
const counter = actor({
@@ -176,6 +178,15 @@ 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" }));
+if (process.env.RIVETKIT_RUNTIME_MODE === "serverless") {
+ await new Promise((resolve, reject) => {
+ const server = serve(
+ { fetch: app.fetch, port: Number(process.env.PORT), hostname: "0.0.0.0" },
+ resolve,
+ );
+ server.once("error", reject);
+ });
+}
export default app;
`,
},