Skip to content
Closed
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
5 changes: 5 additions & 0 deletions crates/agentos-actor-plugin/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ use anyhow::{Context, Result};
/// Serializable mirror of [`AgentOsConfig`]. `deny_unknown_fields` enforces
/// fail-loud behavior when callers pass fields outside this allow-list
/// (including non-serializable fields like `schedule_driver`).
///
/// Keep this struct in sync with
/// `packages/agentos/src/config.ts::nativeAgentOsOptionsSchema` and
/// `packages/agentos/src/actor.ts::buildConfigJson`; TS preflight validation
/// should reject the same native-boundary fields before this serde guard runs.
#[derive(serde::Deserialize, Default, Clone)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(crate) struct AgentOsConfigJson {
Expand Down
3 changes: 3 additions & 0 deletions crates/client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ use serde::{Deserialize, Serialize};
use crate::fs::VirtualFileSystem;

/// Resolved client options (= TS `AgentOsOptions`). All fields optional with documented defaults.
///
/// Keep this Rust mirror in sync with `packages/core/src/agent-os.ts::AgentOsOptions`
/// and `packages/core/src/options-schema.ts::agentOsOptionsSchema`.
#[derive(Default)]
pub struct AgentOsConfig {
/// Software packages to install (flattened). Default `[]`.
Expand Down
14 changes: 7 additions & 7 deletions packages/agentos/src/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
type AgentOsActorConfig,
type AgentOsActorConfigInput,
agentOsActorConfigSchema,
nativeAgentOsOptionsSchema,
} from "./config.js";
import { getPluginPath } from "./plugin-binary.js";
import type { AgentOsActorState, AgentOsActorVars } from "./types.js";
Expand Down Expand Up @@ -191,19 +192,18 @@ function flattenSoftware(input: unknown, out: SoftwareDescriptorLike[]): void {
export function buildConfigJson<TConnParams>(
parsed: AgentOsActorConfig<TConnParams>,
): string {
const options = nativeAgentOsOptionsSchema.parse(
parsed.options ?? {},
) as Record<string, unknown>;
const descriptors: SoftwareDescriptorLike[] = [];
flattenSoftware(
(parsed.options as { software?: unknown })?.software,
descriptors,
);
flattenSoftware(options.software, descriptors);

// Auto-include the default software bundle (`@agentos-software/common`: `sh` +
// coreutils + the standard CLI tools agents rely on) unless the caller opted
// out with `defaultSoftware: false`. Anything already listed in `software`
// (e.g. an explicit `common`) is not duplicated. Prepended so the baseline
// tools come first, matching the previous explicit `[common, ...]` ordering.
const defaultSoftwareEnabled =
(parsed.options as { defaultSoftware?: unknown })?.defaultSoftware !== false;
const defaultSoftwareEnabled = options.defaultSoftware !== false;
if (defaultSoftwareEnabled) {
const defaults: SoftwareDescriptorLike[] = [];
flattenSoftware(common, defaults);
Expand Down Expand Up @@ -233,7 +233,6 @@ export function buildConfigJson<TConnParams>(
// no manual `nodeModulesMount(...)`: see `withAutoAgentNodeModulesMount`. An
// explicit `/root/node_modules` mount in `options.mounts` always wins. The VM
// module resolver reads the mounted tree through the kernel VFS.
const options = (parsed.options ?? {}) as Record<string, unknown>;
const mounts = withAutoAgentNodeModulesMount(
serializeNativeMounts(options.mounts),
descriptors,
Expand All @@ -242,6 +241,7 @@ export function buildConfigJson<TConnParams>(
return JSON.stringify({
software,
additionalInstructions: options.additionalInstructions,
moduleAccessCwd: options.moduleAccessCwd,
loopbackExemptPorts: options.loopbackExemptPorts,
allowedNodeBuiltins: options.allowedNodeBuiltins,
permissions: options.permissions,
Expand Down
57 changes: 51 additions & 6 deletions packages/agentos/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import type {
AgentOsOptions,
JsonRpcNotification,
NativeMountConfig,
PermissionRequest,
} from "@rivet-dev/agentos-core";
import {
agentOsOptionFieldSchemas,
nativeMountConfigSchema,
sharedSidecarConfigSchema,
} from "@rivet-dev/agentos-core";
import type { ActorContext, BeforeConnectContext } from "rivetkit";
import { z } from "zod/v4";
import type { AgentOsActorState, AgentOsActorVars } from "./types.js";
Expand All @@ -11,13 +17,28 @@ const zFunction = <
T extends (...args: any[]) => any = (...args: unknown[]) => unknown,
>() => z.custom<T>((val) => typeof val === "function");

const AgentOsOptionsSchema = z.custom<AgentOsOptions>(
(val) => typeof val === "object" && val !== null,
);
export const nativeAgentOsOptionsSchema = z
// Native actor options are a JSON-serializable subset of AgentOsOptions.
// Keep this allow-list in sync with buildConfigJson() and
// crates/agentos-actor-plugin/src/config.rs::AgentOsConfigJson.
.object({
software: agentOsOptionFieldSchemas.software,
defaultSoftware: agentOsOptionFieldSchemas.defaultSoftware,
loopbackExemptPorts: agentOsOptionFieldSchemas.loopbackExemptPorts,
allowedNodeBuiltins: agentOsOptionFieldSchemas.allowedNodeBuiltins,
rootFilesystem: agentOsOptionFieldSchemas.rootFilesystem,
mounts: z.array(nativeMountConfigSchema).optional(),
moduleAccessCwd: agentOsOptionFieldSchemas.moduleAccessCwd,
additionalInstructions: agentOsOptionFieldSchemas.additionalInstructions,
permissions: agentOsOptionFieldSchemas.permissions,
sidecar: sharedSidecarConfigSchema.optional(),
limits: agentOsOptionFieldSchemas.limits,
})
.strict();

export const agentOsActorConfigSchema = z
.object({
options: AgentOsOptionsSchema.optional(),
options: nativeAgentOsOptionsSchema.optional(),
preview: z
.object({
defaultExpiresInSeconds: z.number().positive().default(3600),
Expand All @@ -33,6 +54,28 @@ export const agentOsActorConfigSchema = z

// --- Typed config types (generic callbacks overlaid on the Zod schema) ---

/**
* Type mirror of `nativeAgentOsOptionsSchema`.
*
* Keep this in sync with the schema above and the Rust serde mirror at
* `crates/agentos-actor-plugin/src/config.rs::AgentOsConfigJson`.
*/
export type NativeAgentOsOptions = Pick<
AgentOsOptions,
| "software"
| "defaultSoftware"
| "loopbackExemptPorts"
| "allowedNodeBuiltins"
| "rootFilesystem"
| "moduleAccessCwd"
| "additionalInstructions"
| "permissions"
| "limits"
> & {
mounts?: NativeMountConfig[];
sidecar?: { kind: "shared"; pool?: string };
};

type AgentOsActorContext<TConnParams> = ActorContext<
AgentOsActorState,
TConnParams,
Expand Down Expand Up @@ -67,13 +110,15 @@ interface AgentOsActorConfigCallbacks<TConnParams> {
// Parsed config (after Zod defaults/transforms applied).
export type AgentOsActorConfig<TConnParams = undefined> = Omit<
z.infer<typeof agentOsActorConfigSchema>,
"onBeforeConnect" | "onSessionEvent" | "onPermissionRequest"
"options" | "onBeforeConnect" | "onSessionEvent" | "onPermissionRequest"
> &
{ options?: NativeAgentOsOptions } &
AgentOsActorConfigCallbacks<TConnParams>;

// Input config (what users pass in before Zod transforms).
export type AgentOsActorConfigInput<TConnParams = undefined> = Omit<
z.input<typeof agentOsActorConfigSchema>,
"onBeforeConnect" | "onSessionEvent" | "onPermissionRequest"
"options" | "onBeforeConnect" | "onSessionEvent" | "onPermissionRequest"
> &
{ options?: NativeAgentOsOptions } &
AgentOsActorConfigCallbacks<TConnParams>;
27 changes: 24 additions & 3 deletions packages/agentos/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
agentOs as createAgentOs,
type AgentOsActorDefinition,
} from "./actor.js";
import type { AgentOsActorConfigInput } from "./config.js";
import type {
AgentOsActorConfigInput,
NativeAgentOsOptions,
} from "./config.js";

export { setup } from "rivetkit";

Expand All @@ -24,11 +27,17 @@ export { createAgentOs as agentOs };
export {
type AgentOsActorConfig,
type AgentOsActorConfigInput,
type NativeAgentOsOptions,
agentOsActorConfigSchema,
nativeAgentOsOptionsSchema,
} from "./config.js";

export { getPluginPath } from "./plugin-binary.js";

// Re-export the software-definition helper so custom agents/tools/commands can
// be defined without importing @rivet-dev/agentos-core directly.
export { defineSoftware } from "@rivet-dev/agentos-core";

export type {
AgentOsActionContext,
AgentOsActorState,
Expand All @@ -52,12 +61,24 @@ export type {
} from "./types.js";

export type AgentOSActorConfigInput<TConnParams = undefined> =
AgentOsActorConfigInput<TConnParams>["options"];
NativeAgentOsOptions &
Omit<AgentOsActorConfigInput<TConnParams>, "options">;

export function agentOS<TConnParams = undefined>(
config: AgentOSActorConfigInput<TConnParams> = {},
): AgentOsActorDefinition<TConnParams> {
const {
onBeforeConnect,
onSessionEvent,
onPermissionRequest,
preview,
...options
} = config;
return createAgentOs({
options: config,
options,
preview,
onBeforeConnect,
onSessionEvent,
onPermissionRequest,
} as AgentOsActorConfigInput<TConnParams>);
}
71 changes: 69 additions & 2 deletions packages/agentos/tests/actor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
import { createClient } from "rivetkit/client";
import common from "@agentos-software/common";
import {
agentOS,
agentOs,
buildConfigJson,
getPluginPath,
Expand Down Expand Up @@ -306,7 +307,7 @@ describe("@rivet-dev/agentos native plugin package bridge", () => {
test("serializes config and hands plugin paths to the NAPI runtime", () => {
const definition = agentOs({
options: {
additionalInstructions: ["stay deterministic"],
additionalInstructions: "stay deterministic",
loopbackExemptPorts: [4020],
mounts: [nodeModulesMount("/host/project/node_modules")],
sidecar: { kind: "shared", pool: "agentos-smoke" },
Expand All @@ -329,7 +330,7 @@ describe("@rivet-dev/agentos native plugin package bridge", () => {
expect(calls[0].pluginPath).toBe(getPluginPath());
expect(calls[0].sidecarPath).toBe(process.env.AGENTOS_SIDECAR_BIN);
expect(JSON.parse(calls[0].configJson)).toMatchObject({
additionalInstructions: ["stay deterministic"],
additionalInstructions: "stay deterministic",
loopbackExemptPorts: [4020],
sidecar: { pool: "agentos-smoke" },
mounts: [
Expand All @@ -348,6 +349,72 @@ describe("@rivet-dev/agentos native plugin package bridge", () => {
});
});

test("agentOS flat config keeps callbacks outside native VM options", () => {
const definition = agentOS({
defaultSoftware: false,
software: [],
onSessionEvent: () => {},
});
const expectedHandle = Symbol("native-factory") as unknown as ActorFactoryHandle;
const calls: NapiNativePluginOptions[] = [];
const runtime = {
kind: "napi",
createNativePluginFactory(options: NapiNativePluginOptions) {
calls.push(options);
return expectedHandle;
},
} as CoreRuntime;

const handle = definition.nativeFactoryBuilder?.(runtime);

expect(handle).toBe(expectedHandle);
expect(calls).toHaveLength(1);
expect(JSON.parse(calls[0].configJson)).toEqual({ software: [] });
expect(calls[0].configJson).not.toContain("onSessionEvent");
});

test("rejects native actor options that cannot cross the NAPI config boundary", () => {
expect(() =>
agentOs({
options: {
toolKits: [],
} as never,
}),
).toThrow(/toolKits/);

expect(() =>
agentOS({
toolKits: [],
} as never),
).toThrow(/toolKits/);

expect(() =>
agentOs({
options: {
mounts: [{ path: "/data", driver: {} }],
} as never,
}),
).toThrow(/driver/);

expect(() =>
agentOs({
options: {
sidecar: { kind: "explicit", handle: {} },
} as never,
}),
).toThrow(/sidecar/);
});

test("buildConfigJson rejects unknown options instead of dropping them", () => {
expect(() =>
buildConfigJson({
options: {
notARealOption: true,
},
} as never),
).toThrow(/notARealOption/);
});

test("buildConfigJson keeps software descriptors pointed at package roots", () => {
const configJson = buildConfigJson({
options: {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
"googleapis": "^144.0.0",
"isolated-vm": "^6.0.0",
"long-timeout": "^0.1.1",
"minimatch": "^10.2.4"
"minimatch": "^10.2.4",
"zod": "^4.1.11"
},
"devDependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
Expand Down Expand Up @@ -106,7 +107,6 @@
"sandbox-agent": "^0.4.2",
"typescript": "^5.7.2",
"vitest": "^2.1.8",
"ws": "^8.18.0",
"zod": "^4.1.11"
"ws": "^8.18.0"
}
}
10 changes: 10 additions & 0 deletions packages/core/src/agent-os.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
} from "./agent-session-types.js";
import { type HostTool, type ToolKit, validateToolkits } from "./host-tools.js";
import { zodToJsonSchema } from "./host-tools-zod.js";
import { parseAgentOsOptions } from "./options-schema.js";
import type {
JsonRpcNotification,
JsonRpcRequest,
Expand Down Expand Up @@ -480,6 +481,14 @@ function defaultAgentStderrHandler(event: AgentStderrEvent): void {
process.stderr.write(event.chunk);
}

/**
* Public core VM options.
*
* Keep this interface in sync with
* `packages/core/src/options-schema.ts::agentOsOptionsSchema`. The Rivet
* native actor intentionally accepts only a subset via
* `packages/agentos/src/config.ts::nativeAgentOsOptionsSchema`.
*/
export interface AgentOsOptions {
/**
* Software to install in the VM. Each entry provides agents, tools,
Expand Down Expand Up @@ -2535,6 +2544,7 @@ export class AgentOs {
}

static async create(options?: AgentOsOptions): Promise<AgentOs> {
options = parseAgentOsOptions(options);
const software =
options?.defaultSoftware === false
? (options.software ?? [])
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ export {
toolKit,
validateToolkits,
} from "./host-tools.js";
export {
agentOsLimitsSchema,
agentOsOptionFieldSchemas,
agentOsOptionsSchema,
hostToolSchema,
mountConfigSchema,
nativeMountConfigSchema,
parseAgentOsOptions,
permissionsSchema,
rootFilesystemConfigSchema,
sharedSidecarConfigSchema,
sidecarConfigSchema,
toolKitSchema,
} from "./options-schema.js";
export {
createInMemoryLayerStore,
createSnapshotExport,
Expand Down
Loading
Loading