Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions benchmarks/dynamic-apps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@
},
"dependencies": {
"@hono/node-server": "^2.1.1",
"@rivet-dev/agentos-core": "0.2.18",
"@rivet-dev/agentos-toolchain": "0.2.18",
"@rivet-dev/agentos-core": "0.4.3",
"@rivet-dev/agentos-toolchain": "0.4.3",
"@rivet-dev/dynamic-apps": "workspace:*",
"@rivet-dev/dynamic-apps-core": "workspace:*",
"hono": "^4.13.5",
"rivetkit": "2.3.11"
"rivetkit": "2.3.29"
},
"devDependencies": {
"@rivetkit/engine-cli": "2.3.11",
"@rivetkit/engine-cli": "2.3.29",
"@types/node": "^22.19.15",
"get-port": "^7.1.0",
"tsx": "^4.20.6",
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/dynamic-apps/src/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export async function deployActorBenchmarkFixture(
main: "index.js",
dependencies: {
hono: "4.13.5",
rivetkit: "2.3.11",
rivetkit: "2.3.29",
},
}),
"index.js": `
Expand Down
37 changes: 33 additions & 4 deletions docs/content/docs/customize-vm.mdx
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
---
title: "Customize the VM"
description: "Configure the agentOS VM that builds and serves each app."
description: "Configure the agentOS VM that serves each app."
skill: true
---

With [Core](/dynamic-apps/docs/core), use the `vm` option to configure the
agentOS VM that serves each app:
Call `setDynamicAppsVmOptions` during host startup to configure the agentOS VM
that serves each app, including apps with Rivet Actors. Pass a function to give
each app its own permissions and host functions:

```ts
import { setDynamicAppsVmOptions } from "@rivet-dev/dynamic-apps";
import { z } from "zod";

setDynamicAppsVmOptions(({ appId }) => ({
// Block outbound network access from app code.
permissions: { network: "deny" },
// Host functions run in your server, so credentials never enter the VM.
hostFunctions: {
github: {
createIssue: {
inputSchema: z.object({ title: z.string() }),
execute: ({ title }) => createIssueFor(appId, title),
},
},
},
}));
```

VMs are cached per release, so call it before the first request. Changed
options apply only to VMs created afterwards.

Apps with actors can still listen inside the VM and reach Rivet under a `deny`
or rule-set network policy, unless your own rules explicitly deny that access.
App code, including actor code, calls a host function as a global:
`await github.createIssue({ title })`.

With [Core](/dynamic-apps/docs/core), use the `vm` option instead:

```ts
const dynamicApps = createDynamicApps({
// Release hooks omitted.
vm: {
software: [firstPackage, secondPackage],
onAgentStderr: (event) => logger.error(event),
onLimitWarning: (warning) => logger.warn(warning),
},
});
Expand Down
2 changes: 1 addition & 1 deletion examples/apps-multiplayer/fixtures/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"main": "src/index.ts",
"dependencies": {
"hono": "4.13.5",
"rivetkit": "2.3.11"
"rivetkit": "2.3.29"
}
}
2 changes: 1 addition & 1 deletion examples/apps-multiplayer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@hono/node-server": "^2.1.1",
"@rivet-dev/dynamic-apps": "workspace:*",
"hono": "^4.13.5",
"rivetkit": "2.3.11"
"rivetkit": "2.3.29"
},
"devDependencies": {
"@types/node": "^22.20.1",
Expand Down
2 changes: 1 addition & 1 deletion examples/apps-sqlite/fixtures/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"main": "src/index.ts",
"dependencies": {
"hono": "4.13.5",
"rivetkit": "2.3.11"
"rivetkit": "2.3.29"
}
}
2 changes: 1 addition & 1 deletion examples/apps-sqlite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@hono/node-server": "^2.1.1",
"@rivet-dev/dynamic-apps": "workspace:*",
"hono": "^4.13.5",
"rivetkit": "2.3.11"
"rivetkit": "2.3.29"
},
"devDependencies": {
"@types/node": "^22.20.1",
Expand Down
2 changes: 1 addition & 1 deletion examples/apps-workflows/fixtures/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"main": "src/index.ts",
"dependencies": {
"hono": "4.13.5",
"rivetkit": "2.3.11"
"rivetkit": "2.3.29"
}
}
2 changes: 1 addition & 1 deletion examples/apps-workflows/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@hono/node-server": "^2.1.1",
"@rivet-dev/dynamic-apps": "workspace:*",
"hono": "^4.13.5",
"rivetkit": "2.3.11"
"rivetkit": "2.3.29"
},
"devDependencies": {
"@types/node": "^22.20.1",
Expand Down
11 changes: 6 additions & 5 deletions packages/dynamic-apps-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,21 @@
"!dist/package.tar"
],
"scripts": {
"build": "tsc && rm -f dist/package.aospkg && agentos-toolchain pack . --out dist/package --prune-native",
"build": "tsc && rm -f dist/package.aospkg && agentos-toolchain pack . --out dist/package --prune-native --omit-optional",
"check-types": "tsc --noEmit",
"test": "vitest run test/ --passWithNoTests"
},
"dependencies": {
"@hono/node-server": "2.1.1",
"@rivetkit/rivetkit-wasm": "2.3.11",
"@rivetkit/rivetkit-wasm": "2.3.29",
"esbuild-wasm": "0.27.4",
"hono": "4.13.5",
"rivetkit": "2.3.11"
"rivetkit": "2.3.29",
"secure-exec": "0.2.1"
},
"devDependencies": {
"@rivet-dev/agentos-core": "0.2.18",
"@rivet-dev/agentos-toolchain": "0.2.18",
"@rivet-dev/agentos-core": "0.4.3",
"@rivet-dev/agentos-toolchain": "0.4.3",
"@types/node": "^22.19.15",
"typescript": "^5.7.3",
"vitest": "^2.1.9"
Expand Down
3 changes: 1 addition & 2 deletions packages/dynamic-apps-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,13 @@ each request. `ephemeral` mode creates a fresh context per request while reusing
the immutable release VM. Use container isolation between trust domains.

Pass additional VM configuration through `vm`. This supports any
number of agentOS software packages as well as runtime observability callbacks:
number of agentOS software packages as well as the limit warning callback:

```ts
const dynamicApps = createDynamicApps({
// Release hooks omitted.
vm: {
software: [firstPackage, secondPackage],
onAgentStderr: (event) => logger.error(event),
onLimitWarning: (warning) => logger.warn(warning),
},
});
Expand Down
8 changes: 3 additions & 5 deletions packages/dynamic-apps-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,13 @@
"node": ">=22.0.0"
},
"scripts": {
"build": "tsup src/index.ts src/internal.ts --format esm --dts --sourcemap --clean --external @rivet-dev/agentos-core --external @rivet-dev/agentos-toolchain --external @rivet-dev/dynamic-apps-builder --external @agentos-software/sh --external @agentos-software/tar",
"build": "tsup src/index.ts src/internal.ts --format esm --dts --sourcemap --clean --external @rivet-dev/agentos-core --external @rivet-dev/agentos-toolchain --external @rivet-dev/dynamic-apps-builder",
"check-types": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"@agentos-software/sh": "0.2.18",
"@agentos-software/tar": "0.3.5",
"@rivet-dev/agentos-core": "0.2.18",
"@rivet-dev/agentos-toolchain": "0.2.18",
"@rivet-dev/agentos-core": "0.4.3",
"@rivet-dev/agentos-toolchain": "0.4.3",
"@rivet-dev/dynamic-apps-builder": "workspace:0.3.0",
"hono": "^4.13.5"
},
Expand Down
7 changes: 3 additions & 4 deletions packages/dynamic-apps-core/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { createHash } from "node:crypto";
import { chmod, mkdtemp, readFile, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import sh from "@agentos-software/sh";
import tar from "@agentos-software/tar";
import {
AgentOs,
type AgentOsOptions,
Expand Down Expand Up @@ -715,8 +713,9 @@ export function createBuildVmFactory(
config: BuildConfig,
): () => Promise<BuildHandle> {
const options: AgentOsOptions = {
defaultSoftware: false,
software: [sh, tar, appsBuilder],
// agentOS vendors the shell and tar the build runs in its default software.
defaultSoftware: true,
software: [appsBuilder],
permissions: {
fs: "allow",
childProcess: "allow",
Expand Down
21 changes: 17 additions & 4 deletions packages/dynamic-apps-core/src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { validateAppId } from "./source.js";
import type {
ActiveRelease,
ApplicationServerRuntime,
DynamicAppsVmContext,
DynamicAppsVmOptions,
ReleaseInvalidation,
ReleaseLoadContext,
Unsubscribe,
Expand Down Expand Up @@ -370,6 +372,14 @@ export function resolveRuntimeAgentOsOptions(
};
}

/** Resolves configured VM options for one release VM. */
export async function resolveVmOptions(
options: DynamicAppsVmOptions | undefined,
context: DynamicAppsVmContext,
): Promise<AgentOsOptions | undefined> {
return typeof options === "function" ? await options(context) : options;
}

function invalidExecutorConfig(name: string): DynamicAppsError {
return new DynamicAppsError(
"dynamic_apps_invalid_config",
Expand All @@ -381,7 +391,7 @@ export class DynamicAppsExecutor {
readonly config: ExecutorConfig;
readonly #source: ExecutorReleaseSource;
readonly #serverRuntime?: ApplicationServerRuntime;
readonly #vmOptions?: AgentOsOptions;
readonly #vmOptions?: DynamicAppsVmOptions;
readonly #semaphore: Semaphore;
readonly #apps = new Map<string, AppCacheEntry>();
readonly #runtimes = new Map<string, PreparedRuntime>();
Expand All @@ -395,7 +405,7 @@ export class DynamicAppsExecutor {
constructor(
source: ExecutorReleaseSource,
config: ExecutorConfig,
vmOptions?: AgentOsOptions,
vmOptions?: DynamicAppsVmOptions,
serverRuntime?: ApplicationServerRuntime,
) {
this.config = config;
Expand Down Expand Up @@ -753,10 +763,13 @@ export class DynamicAppsExecutor {
try {
await chmod(directory, 0o700);
await writeFile(artifactPath, artifact, { mode: 0o600 });
vm = await measureOptional(trace, "vm-prepare", () =>
vm = await measureOptional(trace, "vm-prepare", async () =>
AgentOs.create(
resolveRuntimeAgentOsOptions(
this.#vmOptions,
await resolveVmOptions(this.#vmOptions, {
appId: resolution.appId,
release: resolution.release,
}),
artifactPath,
this.config.contextHeapLimitMb,
),
Expand Down
2 changes: 2 additions & 0 deletions packages/dynamic-apps-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export type {
DynamicApps,
DynamicAppsLogger,
DynamicAppsOptions,
DynamicAppsVmContext,
DynamicAppsVmOptions,
ExecutionMode,
ExecutorConfig,
PublishReleaseInput,
Expand Down
3 changes: 3 additions & 0 deletions packages/dynamic-apps-core/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export {
type ExecutorReleaseSource,
readExecutorConfig,
resolveExecutorConfig,
resolveRuntimeAgentOsOptions,
resolveVmOptions,
} from "./executor.js";
export {
type DynamicAppsLogEvent,
Expand All @@ -28,6 +30,7 @@ export {
DIRECT_ENTRYPOINT,
DIRECT_RUNTIME_FORMAT,
directRunnerSource,
GUEST_FORMAT_ERROR_SOURCE,
normalizeAppPath,
} from "./runtime.js";
export { prepareSource, validateAppId } from "./source.js";
Expand Down
26 changes: 25 additions & 1 deletion packages/dynamic-apps-core/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,21 @@ export async function dispatch(input) {
`;
}

/**
* Guest-side `formatError(error)` that renders an error's stack and cause chain.
* The guest console prints only an error's name and message, so actor runners
* log handler failures through this instead.
*/
export const GUEST_FORMAT_ERROR_SOURCE = `function formatError(error) {
const lines = [];
for (let current = error, depth = 0; current !== undefined && depth < 8; depth++) {
const text = current instanceof Error ? (current.stack ?? String(current)) : String(current);
lines.push(depth === 0 ? text : "Caused by: " + text);
current = current instanceof Error ? current.cause : undefined;
}
return lines.join("\\n");
}`;

/** Initializes RivetKit WASM and hosts the application's exported fetch handler. */
export function actorRunnerSource(entrypointInput: string): string {
const entrypoint = `./${normalizeAppPath(entrypointInput)}`;
Expand All @@ -158,9 +173,18 @@ const fetch = typeof application === "function"
if (!fetch) {
throw new TypeError("Dynamic App entrypoint default export must be a fetch handler");
}
${GUEST_FORMAT_ERROR_SOURCE}
async function handle(request, ...rest) {
try {
return await fetch(request, ...rest);
} catch (error) {
console.error(formatError(error));
return new Response("Internal Server Error", { status: 500 });
}
}
await new Promise((resolve, reject) => {
const server = serve({
fetch,
fetch: handle,
port: Number(process.env.PORT),
hostname: "0.0.0.0",
}, resolve);
Expand Down
18 changes: 17 additions & 1 deletion packages/dynamic-apps-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,30 @@ export interface DynamicAppsOptions<TDeployment, TDeployOptions = undefined> {
): Promise<Unsubscribe>;
executor?: Partial<ExecutorConfig>;
/** Additional options passed to every application-serving VM. */
vm?: AgentOsOptions;
vm?: DynamicAppsVmOptions;
/** Shared HTTP runtime used by releases that contain RivetKit actors. */
serverRuntime?: ApplicationServerRuntime;
build?: Partial<BuildConfig>;
artifactCache?: BuildArtifactCache;
logger?: DynamicAppsLogger;
}

/** The release an application-serving VM is created for. */
export interface DynamicAppsVmContext {
appId: string;
release: string;
}

/**
* agentOS options for application-serving VMs. A function is called once per
* release VM, so options such as permissions or host functions can differ per app.
*/
export type DynamicAppsVmOptions =
| AgentOsOptions
| ((
context: DynamicAppsVmContext,
) => AgentOsOptions | Promise<AgentOsOptions>);

export interface DynamicApps<TDeployment, TDeployOptions = undefined> {
deployApp(
input: DeployAppInput,
Expand Down
6 changes: 2 additions & 4 deletions packages/dynamic-apps-core/tests/agentos-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,19 @@ import { describe, expect, test, vi } from "vitest";
import { resolveRuntimeAgentOsOptions } from "../src/executor.js";

describe("agentOS runtime options", () => {
test("preserves multiple software packages and observability callbacks", () => {
test("preserves multiple software packages and the limit warning callback", () => {
const software = [
{ packagePath: "/packages/one" },
{ packagePath: "/packages/two" },
];
const onAgentStderr = vi.fn();
const onLimitWarning = vi.fn();
const options = resolveRuntimeAgentOsOptions(
{ software, onAgentStderr, onLimitWarning },
{ software, onLimitWarning },
"/tmp/release.aospkg",
96,
);

expect(options.software).toEqual(software);
expect(options.onAgentStderr).toBe(onAgentStderr);
expect(options.onLimitWarning).toBe(onLimitWarning);
expect(options.defaultSoftware).toBe(false);
expect(options.limits?.jsRuntime?.v8HeapLimitMb).toBe(96);
Expand Down
Loading
Loading