diff --git a/CLAUDE.md b/CLAUDE.md index ca2579a3b..53a703064 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,13 +5,40 @@ Agent OS is the agent-facing wrapper around secure-exec. It provides ACP session ## Boundaries - secure-exec dependency workflow. Manage the secure-exec dependency ONLY through `scripts/secure-exec-dep.mjs` (the `just secure-exec-*` recipes); never hand-edit the `path` / `version` / `catalog:` pins. - - Testing against local secure-exec changes: run `just secure-exec-local` to repoint npm (`link:`) and crates (`path = "../secure-exec/..."`) at the sibling checkout, then `node scripts/secure-exec-dep.mjs set-crate-version ` so the Cargo version requirement matches the sibling crate version (otherwise cargo cannot resolve the path deps). Use `just secure-exec-status` to inspect. This mode is for local builds/tests ONLY. + - Testing against local secure-exec changes: run `just secure-exec-local` to repoint npm (`link:`) and crates (`path = "../secure-exec/..."`) at the sibling checkout, then `node scripts/secure-exec-dep.mjs set-crate-version ` so the Cargo version requirement matches the sibling crate version (otherwise cargo cannot resolve the path deps). Also run `pnpm install` in `../secure-exec` first, or cargo panics in `v8-runtime/build.rs` with "missing Node dependencies at .../packages/build-tools/node_modules" (the V8 bridge assets are built from there). Use `just secure-exec-status` to inspect. This mode is for local builds/tests ONLY. - Pushing changes that depend on secure-exec changes: NEVER push with local (`path:` / `link:`) dependencies. First preview-publish the secure-exec changes to their own secure-exec branch (the `preview-publish-secure-exec` flow), then point agent-os back at that exact published version with `just secure-exec-pinned` + `just secure-exec-set-version ` (and `set-crate-version ` for the crates). Only commit/push the pinned-to-remote state. - Keep generic runtime, kernel, VFS, language execution, and registry software behavior in secure-exec. - Agent OS owns ACP, sessions, agent adapters, toolkit semantics, quickstarts, and the AgentOs facade. - Call OS instances VMs, never sandboxes. - The protocol has no backwards compatibility. Clients and the sidecar ship in same-version lockstep, so never add protocol or config versioning, runtime negotiation, fallbacks, or converters. Configs such as `CreateVmConfig` carry no `version` field; the single same-version wire handshake is the only version check. Change the protocol freely and update both sides together. +## Development + +### secure-exec dependency versions (`just`) + +Two independent version tracks: +- **secure-exec** — the `@secure-exec/*` npm packages and the `secure-exec-*` Cargo crates always share **one** version (npm and crates are kept in sync; pin both to the same ``). +- **`@agentos-software/*`** software packages (registry agents / WASM commands) are on a **separate** track and version independently of secure-exec. + +Manage them ONLY via these recipes (never hand-edit `path`/`version`/`catalog:` pins): +- `just secure-exec-local` — point deps at the sibling `../secure-exec` checkout for local hacking. +- `just secure-exec-set-version ` — pin secure-exec to a published version: sets the `@secure-exec/*` npm packages **and** the `secure-exec-*` crates (same ``, they're in sync) and switches to pinned mode. +- `just agentos-pkgs-set-version ` — pin the `@agentos-software/*` software packages (separate version track). + +### Depending on unreleased secure-exec changes + +agent-os builds against secure-exec crates + npm packages, so a secure-exec change must reach agent-os before it can be pushed. NEVER push with local (`path:`/`link:`) deps. Flow: preview-publish the secure-exec branch (the `preview-publish-secure-exec` skill), then `just secure-exec-set-version ` (pins npm + crates + switches to pinned mode), and push only that pinned state. Caveat: a preview publishes npm but the crates.io job is dry-run/skipped — a secure-exec *crate* change only flows locally (`secure-exec-local`) or via a real crates.io release. + +### Preview-publishing agent-os + +`just preview-publish ` dispatches `.github/workflows/publish.yaml` to cut a **preview** (debug build, npm-only, dist-tag = sanitized branch name) — for handing a build to an external project. **Preview-publish is for previews ONLY; never cut a release with it.** Releases go through `just release` (the `scripts/publish` flow). + +### Testing a local build from an external project (same machine) + +To consume an unpublished agent-os build in another project on this machine: +- **npm:** `pnpm -r build`, then either `pnpm pack` the package(s) and `npm install ./rivet-dev-agentos-*.tgz` in the external project, or add a `link:`/`file:` override (e.g. `"@rivet-dev/agentos": "link:/abs/path/agent-os/packages/agentos"`). The sidecar binary ships as `@rivet-dev/agentos-sidecar`. +- **cargo:** point the external Cargo project at the local crate via a path dep or `[patch.crates-io]` override (e.g. `[patch.crates-io] agentos-sidecar = { path = "/abs/path/agent-os/crates/agentos-sidecar" }`). + ## Security Model Trust model (decide which side of the boundary something is on before judging whether it is a security bug). Three components: @@ -41,6 +68,7 @@ Trust model (decide which side of the boundary something is on before judging wh ## Website And Docs +- External/consumer usage (installing `@rivet-dev/agentos` and using it in your own project) is documented in the website quickstart + Agents/Custom Software pages under `website/`, not in this file. This `CLAUDE.md` is contributor/maintainer-only. - The Agent OS website and docs live in `website/` (Astro + Starlight) and deploy to `agentos-sdk.dev` (docs at `agentos-sdk.dev/docs`). The marketing pages and docs were migrated out of `rivet.dev/agent-os` and `rivet.dev/docs/agent-os`, which now 301-redirect to this domain. - Docs styling is owned by the shared **`@rivet-dev/docs-theme`** repo (`github.com/rivet-dev/docs-theme`), consumed via `github:rivet-dev/docs-theme#` and wired in via `...docsTheme(starlight, siteConfig)`. To change any docs styling (palette, header, sidebar, code blocks, fonts), edit that repo and follow its CLAUDE.md release workflow — never restyle docs in `website/src`. This site owns only content + `website/docs.config.mjs` (sidebar icons via each item's `attrs['data-icon']`). - Architecture reference docs live in `website/src/content/docs/docs/architecture/` and are surfaced in `website/docs.config.mjs` under Reference → Advanced → Architecture. Treat these pages as the canonical human-facing architecture reference. When architecture behavior changes or new architecture is added, recommend the corresponding docs update to the user; do not proactively edit the docs unless the user asks for docs work or the task explicitly includes it. diff --git a/Cargo.lock b/Cargo.lock index 536634684..3d728de6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,8 @@ dependencies = [ "serde_json", "tokio", "tracing", + "tracing-appender", + "tracing-logfmt", "tracing-subscriber", ] @@ -2177,6 +2179,15 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "maybe-owned" version = "0.3.4" @@ -3446,6 +3457,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "2.0.117" @@ -3683,6 +3700,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -3715,16 +3745,33 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-logfmt" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a250055a3518b5efba928a18ffac8d32d42ea607a9affff4532144cd5b2e378e" +dependencies = [ + "nu-ansi-term", + "time", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "tracing-subscriber" version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] diff --git a/README.md b/README.md index 5a23aac33..a3b25e399 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,14 @@ ## Why agentOS - **Runs inside your process**: No VMs to boot, no containers to pull. Agents start in milliseconds with minimal memory overhead. -- **Embeds in your backend**: Agents call your functions directly via [host tools](https://rivet.dev/docs/agent-os/tools). No network hops, no complex auth between services. +- **Embeds in your backend**: Agents call your functions directly via [bindings](https://agentos-sdk.dev/docs/bindings). No network hops, no complex auth between services. - **Granular security**: Deny-by-default permissions for filesystem, network, and process access. The same isolation technology trusted by browsers worldwide. - **Deploy anywhere**: Just an npm package. Works on your laptop, Rivet Cloud, Railway, Vercel, Kubernetes, or any container platform. - **Open source**: Apache 2.0 licensed. Self-host or use [Rivet Cloud](https://rivet.dev/docs/agent-os/deployment) for managed infrastructure. ### agentOS vs Sandbox -agentOS is a lightweight VM that runs inside your process. Sandboxes are full Linux environments. agentOS integrates agents into your backend with [host tools](https://rivet.dev/docs/agent-os/tools) and granular permissions. Sandboxes give you a full OS for browsers, native binaries, and dev servers. +agentOS is a lightweight VM that runs inside your process. Sandboxes are full Linux environments. agentOS integrates agents into your backend with [bindings](https://agentos-sdk.dev/docs/bindings) and granular permissions. Sandboxes give you a full OS for browsers, native binaries, and dev servers. You don't have to choose: agentOS works with sandboxes through the [sandbox extension](https://rivet.dev/docs/agent-os/sandbox), spinning up a full sandbox on demand and mounting the sandbox's file system when the workload needs it. @@ -114,13 +114,13 @@ All benchmarks compare agentOS against the fastest/cheapest mainstream sandbox p ### Infrastructure - **[Mount external storage as a filesystem](https://rivet.dev/docs/agent-os/filesystem)**: S3-compatible storage, Google Drive, host directories, overlay filesystems, or custom backends -- **[Host tools](https://rivet.dev/docs/agent-os/tools)**: Define JavaScript functions that agents call as CLI commands inside the VM +- **[Bindings](https://agentos-sdk.dev/docs/bindings)**: Define JavaScript functions that agents call as CLI commands inside the VM - **[Cron](https://rivet.dev/docs/agent-os/cron), [webhooks](https://rivet.dev/docs/agent-os/webhooks), and [queues](https://rivet.dev/docs/agent-os/queues)**: Schedule tasks, receive external events, and serialize work with built-in primitives - **[Sandbox extension](https://rivet.dev/docs/agent-os/sandbox)**: Pair with full sandboxes (E2B, Daytona, etc.) for heavy workloads like browsers or native compilation ### Orchestration - **[Multiplayer](https://rivet.dev/docs/agent-os/multiplayer)**: Multiple clients observe and collaborate with the same agent in real time -- **[Agent-to-agent](https://rivet.dev/docs/agent-os/agent-to-agent)**: Agents delegate work to other agents through host-defined tools +- **[Agent-to-agent](https://rivet.dev/docs/agent-os/agent-to-agent)**: Agents delegate work to other agents through host-defined bindings - **[Workflows](https://rivet.dev/docs/agent-os/workflows)**: Chain agent tasks into durable workflows with retries, branching, and resumable execution - **[Authentication](https://rivet.dev/docs/agent-os/authentication)**: Integrate with your existing auth model (API keys, OAuth, JWTs) diff --git a/crates/agentos-actor-plugin/src/config.rs b/crates/agentos-actor-plugin/src/config.rs index 08c2998cc..78ac30e88 100644 --- a/crates/agentos-actor-plugin/src/config.rs +++ b/crates/agentos-actor-plugin/src/config.rs @@ -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 { diff --git a/crates/agentos-sidecar/Cargo.toml b/crates/agentos-sidecar/Cargo.toml index 24d2e9151..fc203e832 100644 --- a/crates/agentos-sidecar/Cargo.toml +++ b/crates/agentos-sidecar/Cargo.toml @@ -18,9 +18,11 @@ agentos-protocol = { workspace = true } serde_json = "1.0" serde_bare = "0.5" secure-exec-sidecar = { workspace = true } -tokio = { version = "1", features = ["sync", "time"] } +tokio = { version = "1", features = ["sync", "time", "macros"] } tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["fmt"] } +tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } +tracing-logfmt = { version = "0.3", features = ["ansi_logs"] } +tracing-appender = "0.2" [dev-dependencies] agentos-bridge = { workspace = true } diff --git a/crates/agentos-sidecar/src/acp_extension.rs b/crates/agentos-sidecar/src/acp_extension.rs index 320cdef3e..77ad81c28 100644 --- a/crates/agentos-sidecar/src/acp_extension.rs +++ b/crates/agentos-sidecar/src/acp_extension.rs @@ -109,22 +109,74 @@ impl AcpExtension { ctx: ExtensionContext<'_>, payload: &[u8], ) -> Result { + use tracing::Instrument as _; let request = decode_request(payload)?; - let response = match request { - AcpRequest::AcpCreateSessionRequest(request) => self.create_session(ctx, request).await, - AcpRequest::AcpGetSessionStateRequest(request) => { - AcpHandlerOutput::response(self.get_session_state(ctx, request).await) + let kind = Self::acp_request_kind(&request); + let start = std::time::Instant::now(); + tracing::info!(target: "agentos_sidecar::acp_extension", kind, "ext request received"); + + let work = async move { + match request { + AcpRequest::AcpCreateSessionRequest(request) => { + self.create_session(ctx, request).await + } + AcpRequest::AcpGetSessionStateRequest(request) => { + AcpHandlerOutput::response(self.get_session_state(ctx, request).await) + } + AcpRequest::AcpCloseSessionRequest(request) => { + AcpHandlerOutput::response(self.close_session(ctx, request).await) + } + AcpRequest::AcpSessionRequest(request) => self.session_request(ctx, request).await, + AcpRequest::AcpResumeSessionRequest(request) => { + self.resume_session(ctx, request).await + } } - AcpRequest::AcpCloseSessionRequest(request) => { - AcpHandlerOutput::response(self.close_session(ctx, request).await) + } + .instrument(tracing::info_span!( + target: "agentos_sidecar::acp_extension", + "ext.request", + kind + )); + + // Stall watchdog: while the request is in flight, warn periodically so a + // hang surfaces as a breadcrumb long before the host's 120s frame + // timeout. This never interrupts the work itself. + tokio::pin!(work); + let response = loop { + tokio::select! { + result = &mut work => break result, + _ = tokio::time::sleep(std::time::Duration::from_secs(10)) => { + tracing::warn!( + target: "agentos_sidecar::acp_extension", + kind, + elapsed_ms = start.elapsed().as_millis() as u64, + "ext request still pending — possible stall before response frame", + ); + } } - AcpRequest::AcpSessionRequest(request) => self.session_request(ctx, request).await, - AcpRequest::AcpResumeSessionRequest(request) => self.resume_session(ctx, request).await, }; + + tracing::info!( + target: "agentos_sidecar::acp_extension", + kind, + elapsed_ms = start.elapsed().as_millis() as u64, + "ext request handled", + ); let payload = encode_response(response.response.unwrap_or_else(error_response))?; ExtensionResponse::with_wire_events(payload, response.events) } + /// Stable label for an ACP request kind, used as a tracing field. + fn acp_request_kind(request: &AcpRequest) -> &'static str { + match request { + AcpRequest::AcpCreateSessionRequest(_) => "create_session", + AcpRequest::AcpGetSessionStateRequest(_) => "get_session_state", + AcpRequest::AcpCloseSessionRequest(_) => "close_session", + AcpRequest::AcpSessionRequest(_) => "session_request", + AcpRequest::AcpResumeSessionRequest(_) => "resume_session", + } + } + async fn create_session( &self, mut ctx: ExtensionContext<'_>, diff --git a/crates/agentos-sidecar/src/main.rs b/crates/agentos-sidecar/src/main.rs index cdc17b202..8b5c9a153 100644 --- a/crates/agentos-sidecar/src/main.rs +++ b/crates/agentos-sidecar/src/main.rs @@ -1,13 +1,8 @@ +use tracing_subscriber::fmt::writer::BoxMakeWriter; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + fn main() { - // The sidecar speaks its binary protocol over stdout (see - // `secure_exec_sidecar::stdio`), so tracing output MUST go to stderr — - // otherwise log lines are interleaved into the frame stream and the client - // misreads them as (garbage-length) frames. See crates/CLAUDE.md: - // "Control channels must be out-of-band." - tracing_subscriber::fmt() - .with_writer(std::io::stderr) - .with_max_level(tracing::Level::ERROR) - .init(); + init_tracing(); if let Err(error) = secure_exec_sidecar::stdio::run_with_extensions(agentos_sidecar_wrapper::extensions()) { @@ -15,3 +10,81 @@ fn main() { std::process::exit(1); } } + +/// `1` => true, anything else => false. Mirrors rivet's `env_flag`. +fn env_flag(name: &str) -> bool { + std::env::var(name).map_or(false, |v| v == "1") +} + +/// Initialize tracing for the sidecar. +/// +/// Mirrors the rivet logging setup (`rivetkit-napi::init_tracing`): an +/// `EnvFilter`-gated subscriber with a logfmt formatter, so log level and +/// verbosity are runtime-configurable instead of hardcoded. +/// +/// Configuration (all optional): +/// - level: `AGENTOS_LOG_LEVEL` > `LOG_LEVEL` > `RUST_LOG` > `"info"`. +/// - format: `RUST_LOG_FORMAT=logfmt` (default) or `text`. +/// - sink: `AGENTOS_LOG_FILE` (append to that file) else stderr. +/// - field toggles: `RUST_LOG_{SPAN_NAME,SPAN_PATH,TARGET,LOCATION,MODULE_PATH,ANSI_COLOR}`. +/// +/// The sink MUST be stderr or a file — NEVER stdout, which carries the +/// sidecar's binary frame protocol (see crates/CLAUDE.md: "Control channels +/// must be out-of-band"). +fn init_tracing() { + // Level priority: AGENTOS_LOG_LEVEL > LOG_LEVEL > RUST_LOG > "info". + let directive = std::env::var("AGENTOS_LOG_LEVEL") + .ok() + .or_else(|| std::env::var("LOG_LEVEL").ok()) + .or_else(|| std::env::var("RUST_LOG").ok()) + .unwrap_or_else(|| "info".to_string()); + let env_filter = EnvFilter::try_new(&directive).unwrap_or_else(|_| EnvFilter::new("info")); + + // Sink: a file if AGENTOS_LOG_FILE is set, else stderr. Never stdout. + let writer: BoxMakeWriter = match std::env::var("AGENTOS_LOG_FILE") { + Ok(path) if !path.is_empty() => { + let path = std::path::PathBuf::from(path); + let dir = path + .parent() + .filter(|p| !p.as_os_str().is_empty()) + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| std::path::PathBuf::from(".")); + let name = path + .file_name() + .map(std::ffi::OsString::from) + .unwrap_or_else(|| std::ffi::OsString::from("agentos-sidecar.log")); + BoxMakeWriter::new(tracing_appender::rolling::never(dir, name)) + } + _ => BoxMakeWriter::new(std::io::stderr), + }; + + let registry = tracing_subscriber::registry().with(env_filter); + + let text_format = std::env::var("RUST_LOG_FORMAT") + .map(|v| v.eq_ignore_ascii_case("text")) + .unwrap_or(false); + + if text_format { + registry + .with( + tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_writer(writer), + ) + .init(); + } else { + registry + .with( + tracing_logfmt::builder() + .with_span_name(env_flag("RUST_LOG_SPAN_NAME")) + .with_span_path(env_flag("RUST_LOG_SPAN_PATH")) + .with_target(env_flag("RUST_LOG_TARGET")) + .with_location(env_flag("RUST_LOG_LOCATION")) + .with_module_path(env_flag("RUST_LOG_MODULE_PATH")) + .with_ansi_color(env_flag("RUST_LOG_ANSI_COLOR")) + .layer() + .with_writer(writer), + ) + .init(); + } +} diff --git a/crates/client/src/config.rs b/crates/client/src/config.rs index 7a61f6f7d..fb8ddee43 100644 --- a/crates/client/src/config.rs +++ b/crates/client/src/config.rs @@ -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 `[]`. diff --git a/justfile b/justfile index 47ffc0813..f4ef757ca 100644 --- a/justfile +++ b/justfile @@ -6,34 +6,22 @@ release *args: preview-publish REF: gh workflow run .github/workflows/publish.yaml --ref "{{ REF }}" -# Point the workspace at PUBLISHED secure-exec versions (CI/release default). -secure-exec-pinned: - node scripts/secure-exec-dep.mjs pinned - -# Point the workspace at the sibling ../secure-exec checkout for local hacking. +# Point deps at the sibling ../secure-exec checkout for local hacking. secure-exec-local: node scripts/secure-exec-dep.mjs local -# Bump the pinned @secure-exec/* npm version (core/s3/google-drive/sandbox). +# Pin secure-exec to a published version. The @secure-exec/* npm packages and the +# secure-exec-* crates are always the same version, so this sets both and switches +# to pinned mode. secure-exec-set-version VERSION: + node scripts/secure-exec-dep.mjs pinned node scripts/secure-exec-dep.mjs set-secure-exec-version "{{ VERSION }}" + node scripts/secure-exec-dep.mjs set-crate-version "{{ VERSION }}" -# Bump the pinned @agentos-software/* software-package npm version. +# Pin the @agentos-software/* software packages (separate version track). agentos-pkgs-set-version VERSION: node scripts/secure-exec-dep.mjs set-agentos-pkgs-version "{{ VERSION }}" -# Bump BOTH scopes at once (only when secure-exec + software publish in lockstep). -secure-exec-set-all-versions VERSION: - node scripts/secure-exec-dep.mjs set-version "{{ VERSION }}" - -# Bump the @secure-exec/* crate version requirement (must match the sibling crate version). -secure-exec-set-crate-version VERSION: - node scripts/secure-exec-dep.mjs set-crate-version "{{ VERSION }}" - -# Show the current secure-exec dependency mode + pinned versions. -secure-exec-status: - node scripts/secure-exec-dep.mjs status - dev-shell *args: pnpm --filter @rivet-dev/agentos-dev-shell dev-shell -- "$@" diff --git a/packages/agentos/src/actor.ts b/packages/agentos/src/actor.ts index 59dc68a92..056d39479 100644 --- a/packages/agentos/src/actor.ts +++ b/packages/agentos/src/actor.ts @@ -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"; @@ -191,19 +192,18 @@ function flattenSoftware(input: unknown, out: SoftwareDescriptorLike[]): void { export function buildConfigJson( parsed: AgentOsActorConfig, ): string { + const options = nativeAgentOsOptionsSchema.parse( + parsed.options ?? {}, + ) as Record; 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); @@ -233,7 +233,6 @@ export function buildConfigJson( // 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; const mounts = withAutoAgentNodeModulesMount( serializeNativeMounts(options.mounts), descriptors, @@ -242,6 +241,7 @@ export function buildConfigJson( return JSON.stringify({ software, additionalInstructions: options.additionalInstructions, + moduleAccessCwd: options.moduleAccessCwd, loopbackExemptPorts: options.loopbackExemptPorts, allowedNodeBuiltins: options.allowedNodeBuiltins, permissions: options.permissions, diff --git a/packages/agentos/src/config.ts b/packages/agentos/src/config.ts index 575e7ac76..746fffbda 100644 --- a/packages/agentos/src/config.ts +++ b/packages/agentos/src/config.ts @@ -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"; @@ -11,13 +17,28 @@ const zFunction = < T extends (...args: any[]) => any = (...args: unknown[]) => unknown, >() => z.custom((val) => typeof val === "function"); -const AgentOsOptionsSchema = z.custom( - (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), @@ -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 = ActorContext< AgentOsActorState, TConnParams, @@ -67,13 +110,15 @@ interface AgentOsActorConfigCallbacks { // Parsed config (after Zod defaults/transforms applied). export type AgentOsActorConfig = Omit< z.infer, - "onBeforeConnect" | "onSessionEvent" | "onPermissionRequest" + "options" | "onBeforeConnect" | "onSessionEvent" | "onPermissionRequest" > & + { options?: NativeAgentOsOptions } & AgentOsActorConfigCallbacks; // Input config (what users pass in before Zod transforms). export type AgentOsActorConfigInput = Omit< z.input, - "onBeforeConnect" | "onSessionEvent" | "onPermissionRequest" + "options" | "onBeforeConnect" | "onSessionEvent" | "onPermissionRequest" > & + { options?: NativeAgentOsOptions } & AgentOsActorConfigCallbacks; diff --git a/packages/agentos/src/index.ts b/packages/agentos/src/index.ts index 8b7b9c46e..aefd1bfcd 100644 --- a/packages/agentos/src/index.ts +++ b/packages/agentos/src/index.ts @@ -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"; @@ -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, @@ -52,12 +61,24 @@ export type { } from "./types.js"; export type AgentOSActorConfigInput = - AgentOsActorConfigInput["options"]; + NativeAgentOsOptions & + Omit, "options">; export function agentOS( config: AgentOSActorConfigInput = {}, ): AgentOsActorDefinition { + const { + onBeforeConnect, + onSessionEvent, + onPermissionRequest, + preview, + ...options + } = config; return createAgentOs({ - options: config, + options, + preview, + onBeforeConnect, + onSessionEvent, + onPermissionRequest, } as AgentOsActorConfigInput); } diff --git a/packages/agentos/tests/actor.test.ts b/packages/agentos/tests/actor.test.ts index 3e5c4bfd5..17919f636 100644 --- a/packages/agentos/tests/actor.test.ts +++ b/packages/agentos/tests/actor.test.ts @@ -14,6 +14,7 @@ import type { import { createClient } from "rivetkit/client"; import common from "@agentos-software/common"; import { + agentOS, agentOs, buildConfigJson, getPluginPath, @@ -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" }, @@ -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: [ @@ -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: { diff --git a/packages/core/package.json b/packages/core/package.json index db0186e68..ea73648b0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -65,7 +65,9 @@ "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", + "zod-to-json-schema": "^3.25.2" }, "devDependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.87", @@ -107,6 +109,6 @@ "typescript": "^5.7.2", "vitest": "^2.1.8", "ws": "^8.18.0", - "zod": "^4.1.11" + "zod3": "npm:zod@^3.25.76" } } diff --git a/packages/core/src/agent-os.ts b/packages/core/src/agent-os.ts index f8f6e8348..c7f1d10e8 100644 --- a/packages/core/src/agent-os.ts +++ b/packages/core/src/agent-os.ts @@ -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, @@ -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, @@ -2535,6 +2544,7 @@ export class AgentOs { } static async create(options?: AgentOsOptions): Promise { + options = parseAgentOsOptions(options); const software = options?.defaultSoftware === false ? (options.software ?? []) diff --git a/packages/core/src/host-tools-zod.ts b/packages/core/src/host-tools-zod.ts index fe29815e3..5e4078d4e 100644 --- a/packages/core/src/host-tools-zod.ts +++ b/packages/core/src/host-tools-zod.ts @@ -1,4 +1,5 @@ import type { ZodType } from "zod"; +import { zodToJsonSchema as zodV3ToJsonSchema } from "zod-to-json-schema"; const OPTIONAL_WRAPPER_TYPES = new Set(["default", "optional"]); const TRANSPARENT_WRAPPER_TYPES = new Set([ @@ -40,6 +41,9 @@ export class HostToolSchemaConversionError extends Error { } function getSchemaDef(schema: unknown): JsonObject { + if (!schema || typeof schema !== "object") { + return {}; + } return (( schema as { _def?: JsonObject; @@ -195,7 +199,8 @@ function validateSchema(schema: ZodType, path: string) { } if (typeName === "array") { - const itemSchema = getSchemaDef(schema).element as ZodType | undefined; + const def = getSchemaDef(schema); + const itemSchema = (def.element ?? def.type) as ZodType | undefined; if (!itemSchema) { throw new HostToolSchemaConversionError( path, @@ -246,8 +251,12 @@ function validateSchema(schema: ZodType, path: string) { } if (typeName === "literal") { - const values = getSchemaDef(schema).values; - const literalValues = Array.isArray(values) ? values : []; + const def = getSchemaDef(schema); + const literalValues = Array.isArray(def.values) + ? def.values + : "value" in def + ? [def.value] + : []; const [literalValue] = literalValues; if ( literalValues.length !== 1 || @@ -331,19 +340,32 @@ function findUnsupportedGeneratedKeyword( return null; } -export function zodToJsonSchema(schema: ZodType): unknown { - validateSchema(schema, "$"); - - const jsonSchema = ( +function generateJsonSchema(schema: ZodType): unknown { + const nativeJsonSchema = ( schema as ZodType & { toJSONSchema?: () => unknown } ).toJSONSchema?.(); - if (!jsonSchema) { + if (nativeJsonSchema) { + return nativeJsonSchema; + } + + const generated = zodV3ToJsonSchema(schema as never, { + $refStrategy: "none", + target: "jsonSchema7", + }); + if (!generated || (typeof generated === "object" && Object.keys(generated).length === 0)) { throw new HostToolSchemaConversionError( "$", displayTypeName(normalizeTypeName(schema)), - "schema does not expose toJSONSchema()", + "schema cannot be converted to JSON Schema", ); } + return generated; +} + +export function zodToJsonSchema(schema: ZodType): unknown { + validateSchema(schema, "$"); + + const jsonSchema = generateJsonSchema(schema); const unsupportedKeyword = findUnsupportedGeneratedKeyword(jsonSchema, "$"); if (unsupportedKeyword) { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fdf2c0099..9aa036476 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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, diff --git a/packages/core/src/options-schema.ts b/packages/core/src/options-schema.ts new file mode 100644 index 000000000..53b4af68e --- /dev/null +++ b/packages/core/src/options-schema.ts @@ -0,0 +1,296 @@ +import { z } from "zod/v4"; +import type { + AgentOsOptions, + AgentStderrHandler, + NativeMountConfig, +} from "./agent-os.js"; +import type { HostTool, ToolKit } from "./host-tools.js"; + +const stringArray = z.array(z.string()); +const nonNegativeInteger = z.number().int().nonnegative(); +const positiveInteger = z.number().int().positive(); +const functionSchema = z.custom<(...args: any[]) => any>( + (value) => typeof value === "function", + { message: "Expected function" }, +); + +const permissionModeSchema = z.enum(["allow", "deny"]); + +const fsPermissionRuleSchema = z + .object({ + mode: permissionModeSchema, + operations: stringArray.optional(), + paths: stringArray.optional(), + }) + .strict(); + +const patternPermissionRuleSchema = z + .object({ + mode: permissionModeSchema, + operations: stringArray.optional(), + patterns: stringArray.optional(), + }) + .strict(); + +const fsRulePermissionsSchema = z + .object({ + default: permissionModeSchema.optional(), + rules: z.array(fsPermissionRuleSchema), + }) + .strict(); + +const patternRulePermissionsSchema = z + .object({ + default: permissionModeSchema.optional(), + rules: z.array(patternPermissionRuleSchema), + }) + .strict(); + +const fsPermissionsSchema = z.union([permissionModeSchema, fsRulePermissionsSchema]); +const patternPermissionsSchema = z.union([ + permissionModeSchema, + patternRulePermissionsSchema, +]); + +export const permissionsSchema = z + .object({ + fs: fsPermissionsSchema.optional(), + network: patternPermissionsSchema.optional(), + childProcess: patternPermissionsSchema.optional(), + process: patternPermissionsSchema.optional(), + env: patternPermissionsSchema.optional(), + tool: patternPermissionsSchema.optional(), + }) + .strict(); + +export const agentOsLimitsSchema = z + .object({ + resources: z + .object({ + cpuCount: positiveInteger.optional(), + maxProcesses: nonNegativeInteger.optional(), + maxOpenFds: nonNegativeInteger.optional(), + maxPipes: nonNegativeInteger.optional(), + maxPtys: nonNegativeInteger.optional(), + maxSockets: nonNegativeInteger.optional(), + maxConnections: nonNegativeInteger.optional(), + maxSocketBufferedBytes: nonNegativeInteger.optional(), + maxSocketDatagramQueueLen: nonNegativeInteger.optional(), + maxFilesystemBytes: nonNegativeInteger.optional(), + maxInodeCount: nonNegativeInteger.optional(), + maxBlockingReadMs: nonNegativeInteger.optional(), + maxPreadBytes: nonNegativeInteger.optional(), + maxFdWriteBytes: nonNegativeInteger.optional(), + maxProcessArgvBytes: nonNegativeInteger.optional(), + maxProcessEnvBytes: nonNegativeInteger.optional(), + maxReaddirEntries: nonNegativeInteger.optional(), + maxWasmFuel: nonNegativeInteger.optional(), + maxWasmMemoryBytes: nonNegativeInteger.optional(), + maxWasmStackBytes: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + http: z + .object({ maxFetchResponseBytes: nonNegativeInteger.optional() }) + .strict() + .optional(), + tools: z + .object({ + defaultToolTimeoutMs: nonNegativeInteger.optional(), + maxToolTimeoutMs: nonNegativeInteger.optional(), + maxRegisteredToolkits: nonNegativeInteger.optional(), + maxRegisteredToolsPerVm: nonNegativeInteger.optional(), + maxToolsPerToolkit: nonNegativeInteger.optional(), + maxToolSchemaBytes: nonNegativeInteger.optional(), + maxToolExamplesPerTool: nonNegativeInteger.optional(), + maxToolExampleInputBytes: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + plugins: z + .object({ + maxPersistedManifestBytes: nonNegativeInteger.optional(), + maxPersistedManifestFileBytes: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + acp: z + .object({ + maxReadLineBytes: nonNegativeInteger.optional(), + stdoutBufferByteLimit: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + jsRuntime: z + .object({ + v8HeapLimitMb: nonNegativeInteger.optional(), + capturedOutputLimitBytes: nonNegativeInteger.optional(), + stdinBufferLimitBytes: nonNegativeInteger.optional(), + eventPayloadLimitBytes: nonNegativeInteger.optional(), + v8IpcMaxFrameBytes: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + python: z + .object({ + outputBufferMaxBytes: nonNegativeInteger.optional(), + executionTimeoutMs: nonNegativeInteger.optional(), + vfsRpcTimeoutMs: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + wasm: z + .object({ + maxModuleFileBytes: nonNegativeInteger.optional(), + capturedOutputLimitBytes: nonNegativeInteger.optional(), + syncReadLimitBytes: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + }) + .strict(); + +const rootLowerInputSchema = z.union([ + z.object({ kind: z.literal("bundled-base-filesystem") }).strict(), + z.object({ kind: z.literal("snapshot-export"), source: z.unknown() }).strict(), +]); + +export const rootFilesystemConfigSchema = z + .object({ + type: z.literal("overlay").optional(), + mode: z.enum(["ephemeral", "read-only"]).optional(), + disableDefaultBaseLayer: z.boolean().optional(), + lowers: z.array(rootLowerInputSchema).optional(), + }) + .strict(); + +const nativeMountPluginSchema = z + .object({ + id: z.string(), + config: z.unknown().optional(), + }) + .strict(); + +const plainMountConfigSchema = z + .object({ + path: z.string(), + driver: z.custom((value) => typeof value === "object" && value !== null, { + message: "Expected filesystem driver object", + }), + readOnly: z.boolean().optional(), + }) + .strict(); + +export const nativeMountConfigSchema = z + .object({ + path: z.string(), + plugin: nativeMountPluginSchema, + readOnly: z.boolean().optional(), + }) + .strict() as z.ZodType; + +const overlayMountConfigSchema = z + .object({ + path: z.string(), + filesystem: z + .object({ + type: z.literal("overlay"), + store: z.unknown(), + mode: z.enum(["ephemeral", "read-only"]).optional(), + lowers: z.array(z.unknown()), + }) + .strict(), + }) + .strict(); + +export const mountConfigSchema = z.union([ + plainMountConfigSchema, + nativeMountConfigSchema, + overlayMountConfigSchema, +]); + +export const sharedSidecarConfigSchema = z + .object({ + kind: z.literal("shared"), + pool: z.string().optional(), + }) + .strict(); + +const explicitSidecarSchema = z + .object({ + kind: z.literal("explicit"), + handle: z.unknown(), + }) + .strict(); + +export const sidecarConfigSchema = z.union([ + sharedSidecarConfigSchema, + explicitSidecarSchema, +]); + +const toolExampleSchema = z + .object({ + description: z.string(), + input: z.unknown(), + }) + .strict(); + +export const hostToolSchema = z + .object({ + description: z.string(), + inputSchema: z.custom((value) => typeof value === "object" && value !== null, { + message: "Expected Zod schema object", + }), + execute: functionSchema, + examples: z.array(toolExampleSchema).optional(), + timeout: nonNegativeInteger.optional(), + }) + .strict() as z.ZodType; + +export const toolKitSchema = z + .object({ + name: z.string(), + description: z.string(), + tools: z.record(z.string(), hostToolSchema), + }) + .strict() as z.ZodType; + +/** + * Shared AgentOsOptions field schemas. + * + * Core uses the full object. The Rivet/native actor composes a narrower + * native-safe subset in `packages/agentos/src/config.ts`; keep that subset and + * `crates/agentos-actor-plugin/src/config.rs::AgentOsConfigJson` aligned when + * adding options that cross the native boundary. + */ +export const agentOsOptionFieldSchemas = { + software: z.array(z.unknown()).optional(), + defaultSoftware: z.boolean().optional(), + loopbackExemptPorts: z.array(z.number().int().min(0).max(65535)).optional(), + allowedNodeBuiltins: stringArray.optional(), + rootFilesystem: rootFilesystemConfigSchema.optional(), + mounts: z.array(mountConfigSchema).optional(), + moduleAccessCwd: z.string().optional(), + additionalInstructions: z.string().optional(), + scheduleDriver: z + .custom((value) => typeof value === "object" && value !== null, { + message: "Expected schedule driver object", + }) + .optional(), + toolKits: z.array(toolKitSchema).optional(), + permissions: permissionsSchema.optional(), + sidecar: sidecarConfigSchema.optional(), + limits: agentOsLimitsSchema.optional(), + onAgentStderr: z.custom( + (value) => typeof value === "function", + { message: "Expected function" }, + ).optional(), +} as const; + +export const agentOsOptionsSchema = z + .object(agentOsOptionFieldSchemas) + .strict() as z.ZodType; + +export function parseAgentOsOptions(options?: AgentOsOptions): AgentOsOptions { + return agentOsOptionsSchema.parse(options ?? {}); +} diff --git a/packages/core/tests/host-tools-zod.test.ts b/packages/core/tests/host-tools-zod.test.ts index 96023ca08..e9580dad5 100644 --- a/packages/core/tests/host-tools-zod.test.ts +++ b/packages/core/tests/host-tools-zod.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vitest"; import { z } from "zod"; +import { z as z3 } from "zod3"; import { HostToolSchemaConversionError, zodToJsonSchema, @@ -87,6 +88,60 @@ describe("zodToJsonSchema", () => { }); }); + test("converts equivalent Zod v3 schemas", () => { + const schema = z3.object({ + url: z3 + .string() + .min(1) + .max(128) + .regex(/^https?:\/\//) + .describe("Target URL"), + fullPage: z3.boolean().optional(), + format: z3.enum(["png", "jpg"]).describe("Image format"), + width: z3.number().min(320).max(1920).optional(), + tags: z3.array(z3.string()), + mode: z3.union([z3.literal("fast"), z3.literal("safe")]), + env: z3.record(z3.string(), z3.string()).optional(), + }); + + expect(zodToJsonSchema(schema)).toEqual({ + type: "object", + properties: { + url: { + type: "string", + minLength: 1, + maxLength: 128, + pattern: "^https?:\\/\\/", + description: "Target URL", + }, + fullPage: { type: "boolean" }, + format: { + type: "string", + enum: ["png", "jpg"], + description: "Image format", + }, + width: { + type: "number", + minimum: 320, + maximum: 1920, + }, + tags: { + type: "array", + items: { type: "string" }, + }, + mode: { + type: "string", + enum: ["fast", "safe"], + }, + env: { + type: "object", + additionalProperties: { type: "string" }, + }, + }, + required: ["url", "format", "tags", "mode"], + }); + }); + test("throws a typed error for unsupported discriminated unions with the offending path", () => { const schema = z.object({ payload: z.discriminatedUnion("kind", [ diff --git a/packages/core/tests/options-schema.test.ts b/packages/core/tests/options-schema.test.ts new file mode 100644 index 000000000..e2fbe8421 --- /dev/null +++ b/packages/core/tests/options-schema.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "vitest"; +import { AgentOs, agentOsOptionsSchema } from "../src/index.js"; + +describe("AgentOsOptions validation", () => { + test("rejects unknown top-level options before booting a VM", async () => { + await expect( + AgentOs.create({ + onSessionEvent: () => {}, + } as never), + ).rejects.toThrow(/onSessionEvent/); + }); + + test("rejects unknown nested permission fields", () => { + expect(() => + agentOsOptionsSchema.parse({ + permissions: { + filesystem: "allow", + }, + }), + ).toThrow(/filesystem/); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e4757555..74d4c2f99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,20 +82,20 @@ catalogs: specifier: 0.3.0-rc.2 version: 0.3.0-rc.2 '@secure-exec/core': - specifier: 0.0.0-nathan-workspace-binding-agentos.83db3e3 - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + specifier: 0.0.0-codex-native-sidecar-platform-ci.117d645 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/google-drive': - specifier: 0.0.0-nathan-workspace-binding-agentos.83db3e3 - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + specifier: 0.0.0-codex-native-sidecar-platform-ci.117d645 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/nodejs': specifier: 0.2.1 version: 0.2.1 '@secure-exec/s3': - specifier: 0.0.0-nathan-workspace-binding-agentos.83db3e3 - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + specifier: 0.0.0-codex-native-sidecar-platform-ci.117d645 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/sandbox': - specifier: 0.0.0-nathan-workspace-binding-agentos.83db3e3 - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + specifier: 0.0.0-codex-native-sidecar-platform-ci.117d645 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 overrides: '@rivet-dev/agentos-core': workspace:* @@ -128,7 +128,7 @@ importers: version: link:packages/core '@secure-exec/core': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@types/node': specifier: ^22.19.15 version: 22.19.15 @@ -208,7 +208,7 @@ importers: version: link:../../packages/agentos-sandbox '@secure-exec/s3': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 dockerode: specifier: ^4.0.9 version: 4.0.10 @@ -279,7 +279,7 @@ importers: version: link:../core '@secure-exec/sandbox': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6) + version: 0.0.0-codex-native-sidecar-platform-ci.117d645(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6) sandbox-agent: specifier: ^0.4.2 version: 0.4.2(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6) @@ -335,7 +335,7 @@ importers: version: 0.6.2 '@secure-exec/core': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@xterm/headless': specifier: ^6.0.0 version: 6.0.0 @@ -357,6 +357,12 @@ importers: minimatch: specifier: ^10.2.4 version: 10.2.5 + zod: + specifier: ^4.1.11 + version: 4.3.6 + zod-to-json-schema: + specifier: ^3.25.2 + version: 3.25.2(zod@4.3.6) devDependencies: '@agentos-software/claude-code': specifier: 'catalog:' @@ -453,10 +459,10 @@ importers: version: link:../agentos-sandbox '@secure-exec/google-drive': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/s3': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -475,9 +481,9 @@ importers: ws: specifier: ^8.18.0 version: 8.20.0(bufferutil@4.1.0) - zod: - specifier: ^4.1.11 - version: 4.3.6 + zod3: + specifier: npm:zod@^3.25.76 + version: zod@3.25.76 packages/dev-shell: dependencies: @@ -536,7 +542,7 @@ importers: dependencies: '@secure-exec/core': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 devDependencies: '@types/node': specifier: ^22.10.2 @@ -558,7 +564,7 @@ importers: dependencies: '@secure-exec/core': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 pyodide: specifier: ^0.28.3 version: 0.28.3(bufferutil@4.1.0) @@ -2917,50 +2923,50 @@ packages: resolution: {integrity: sha512-trO//ypJBSt5xkewuol9LOykvDgHwUXq8R+yQVS+0CmpN3lYUtewHkb+At9RVGRhDMmJZY2oasaXDnhfurQ33w==} hasBin: true - '@secure-exec/core@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-GKGY8U1wI/R/Rl8WTqQ+xLqu2FALZL0XzfIxolZVhfR/iy7mwkSN5phO6cimM5qjxwSTkTCEEvoVrBoakedRBQ==} + '@secure-exec/core@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-bomEzynHLrGIF+k1S6zIQHJwdQw8V/WfEGKL9nj+iMC8OlfDnaJ26SwV8Yk3M7KwjKpznQsMNmPeIK5iFZ0s8A==} '@secure-exec/core@0.2.1': resolution: {integrity: sha512-HsnUv6gClpMA1BBRmX86j30TKTZtgJC/fO1tVavr7IpM2zNKbHU8LgSlBd7mv2SNy02ImTmU/GnQ3aYB4NSbEg==} - '@secure-exec/google-drive@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-zhd2O7jit5rghIQN8n+GZtpiiwYni8IhNHZOEgWcK0/UlYL5FecdEhGLeCo0rWTXV7WFPGfGPhX3aJOQKgnwOA==} + '@secure-exec/google-drive@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-3s7entAvaBHOYDHpg4g5dbaC91FsbZaHProA0B75kk6+EV2VLb7s0pAgRips9J1slh4RdDOolw6TRd8CrHXHsg==} '@secure-exec/nodejs@0.2.1': resolution: {integrity: sha512-UJMJqVFxexlHJV0Q9nWURvrz6GElj8673DDOOFln6FHR6JS+9SaSU3eISrN158DuNC3SFi4rgjb/scKnK4YOYQ==} - '@secure-exec/s3@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-/4w480tR0UZh0ohUTyjDipkEVchJsflEFPTeeYYBEkDphEoDWz1PlshpAQWBGTkC3sRd9QyhzEsEXVSNMvrmQA==} + '@secure-exec/s3@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-Q5ESnaQkPc1PxDXO//Yysk3MxJOE9xKA2EVvZjKmdqyKyiesgnV1/oik4LKb8JGBYO63mokOqprxk7MO9R7g5w==} - '@secure-exec/sandbox@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-yVninZkFt4BHtgNX2d3bGKqlORdlrIXvQLpOnqpaeMIHbP0IJSbGd7jRbk4HAFYRo8JKcEHo/dr8dOUBUF9lPQ==} + '@secure-exec/sandbox@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-zjwLoKGIJhSggKfEdcRrXv6AXJUiFvjCCPWy/v3R2uGr5EZTUp6KsECmzggzcT/6OxN54A46Eb/8ysZgEiyfbA==} - '@secure-exec/sidecar-darwin-arm64@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-3YRc7fe9tt7HRpeWXN6UXaiswLOfRmuUrLJkKPL03Bbxw+sOOiHNBzcmVRkoR4nMOfrv11sAwudIlydSylxW1Q==} + '@secure-exec/sidecar-darwin-arm64@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-cQe2UnN7lj4htBSablaPCv6kmzybUADXyWCfRRQKKzV5UD204+Hh63wtZ/wyzs4N7kQaoGqtVSNfskR0c4/Z+A==} engines: {node: '>=20'} cpu: [arm64] os: [darwin] - '@secure-exec/sidecar-darwin-x64@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-oGN8HWR6VnL22BjZeoJ/MVQTqGWzfLhpx4x4Pjw11cX6FU7/7sEP54zHfp8q+8f7zOHI0+plDOHXYEQ+mIi15g==} + '@secure-exec/sidecar-darwin-x64@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-KfDXmf65TDgD9fKA/fuIpqgsSmy6mGWfvKNGOELNii9aAj19aw5DKt0Sh9AjL4BVTkKxlkQRfhctTuYTF+LyzQ==} engines: {node: '>=20'} cpu: [x64] os: [darwin] - '@secure-exec/sidecar-linux-arm64-gnu@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-+V75dmjpM87mjqW0oJZqbwCAqirKFeP6hRRIb+Ujrqy4rBuWCDC8QZdLZacF0S7gn8sWW7GcbBScTY2mgb2Q4A==} + '@secure-exec/sidecar-linux-arm64-gnu@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-antjZRycgVuCcnQRoZV+By0isj6d1d0s/MbZs8t6Ssz4NNJKHeiRZUMEZ8H70Irywh1iFLcilP7hfau1nA4z5w==} engines: {node: '>=20'} cpu: [arm64] os: [linux] - '@secure-exec/sidecar-linux-x64-gnu@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-OS+dS3WnX5HeBI0deeL/0weLeSUGGLofYM4pklemjH19V1TynGs2eZw4Wrkeh2IRmsK+DtNGwZ730yra4AVW8g==} + '@secure-exec/sidecar-linux-x64-gnu@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-sXlgTa7r9x0Y8g35RJbUqwk3V7J/Rz/U6p4kMQ77tD+j7tj4L0sxDZn8n02z40iA7+OFD58BImoKOUNHOzEMkg==} engines: {node: '>=20'} cpu: [x64] os: [linux] - '@secure-exec/sidecar@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-TsRIzKQfBH6CBRBdY17pHwZudMieuMar2xoLssbOGKnb3C5BvKMt/3yaANerPxVxH3LYs8KLjyLaCujSaRErSA==} + '@secure-exec/sidecar@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-kij9InI4RXl2ZLPZKhtcNMYiq/wQQjejq5yOuJnXULeH9iNG7IrGeQ6bMh346ef7mxUL1XnT7Ptv57gB8cWukg==} engines: {node: '>=20'} '@secure-exec/v8-darwin-arm64@0.2.1': @@ -9697,18 +9703,19 @@ snapshots: '@sandbox-agent/cli-win32-x64': 0.4.2 optional: true - '@secure-exec/core@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/core@0.0.0-codex-native-sidecar-platform-ci.117d645': dependencies: '@rivetkit/bare-ts': 0.6.2 - '@secure-exec/sidecar': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/sidecar': 0.0.0-codex-native-sidecar-platform-ci.117d645 + zod: 4.3.6 '@secure-exec/core@0.2.1': dependencies: better-sqlite3: 12.8.0 - '@secure-exec/google-drive@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/google-drive@0.0.0-codex-native-sidecar-platform-ci.117d645': dependencies: - '@secure-exec/core': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/core': 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/nodejs@0.2.1': dependencies: @@ -9721,13 +9728,13 @@ snapshots: node-stdlib-browser: 1.3.1 web-streams-polyfill: 4.2.0 - '@secure-exec/s3@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/s3@0.0.0-codex-native-sidecar-platform-ci.117d645': dependencies: - '@secure-exec/core': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/core': 0.0.0-codex-native-sidecar-platform-ci.117d645 - '@secure-exec/sandbox@0.0.0-nathan-workspace-binding-agentos.83db3e3(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6)': + '@secure-exec/sandbox@0.0.0-codex-native-sidecar-platform-ci.117d645(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6)': dependencies: - '@secure-exec/core': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/core': 0.0.0-codex-native-sidecar-platform-ci.117d645 sandbox-agent: 0.4.2(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6) transitivePeerDependencies: - '@cloudflare/sandbox' @@ -9741,24 +9748,24 @@ snapshots: - modal - zod - '@secure-exec/sidecar-darwin-arm64@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/sidecar-darwin-arm64@0.0.0-codex-native-sidecar-platform-ci.117d645': optional: true - '@secure-exec/sidecar-darwin-x64@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/sidecar-darwin-x64@0.0.0-codex-native-sidecar-platform-ci.117d645': optional: true - '@secure-exec/sidecar-linux-arm64-gnu@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/sidecar-linux-arm64-gnu@0.0.0-codex-native-sidecar-platform-ci.117d645': optional: true - '@secure-exec/sidecar-linux-x64-gnu@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/sidecar-linux-x64-gnu@0.0.0-codex-native-sidecar-platform-ci.117d645': optional: true - '@secure-exec/sidecar@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/sidecar@0.0.0-codex-native-sidecar-platform-ci.117d645': optionalDependencies: - '@secure-exec/sidecar-darwin-arm64': 0.0.0-nathan-workspace-binding-agentos.83db3e3 - '@secure-exec/sidecar-darwin-x64': 0.0.0-nathan-workspace-binding-agentos.83db3e3 - '@secure-exec/sidecar-linux-arm64-gnu': 0.0.0-nathan-workspace-binding-agentos.83db3e3 - '@secure-exec/sidecar-linux-x64-gnu': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/sidecar-darwin-arm64': 0.0.0-codex-native-sidecar-platform-ci.117d645 + '@secure-exec/sidecar-darwin-x64': 0.0.0-codex-native-sidecar-platform-ci.117d645 + '@secure-exec/sidecar-linux-arm64-gnu': 0.0.0-codex-native-sidecar-platform-ci.117d645 + '@secure-exec/sidecar-linux-x64-gnu': 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/v8-darwin-arm64@0.2.1': optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2d7787290..b097d6963 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -59,9 +59,9 @@ catalog: '@agentos-software/unzip': 0.3.0-rc.2 '@agentos-software/yq': 0.3.0-rc.2 '@agentos-software/zip': 0.3.0-rc.2 - '@secure-exec/core': 0.0.0-nathan-workspace-binding-agentos.83db3e3 - '@secure-exec/google-drive': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/core': 0.0.0-codex-native-sidecar-platform-ci.117d645 + '@secure-exec/google-drive': 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/nodejs': 0.2.1 - '@secure-exec/s3': 0.0.0-nathan-workspace-binding-agentos.83db3e3 - '@secure-exec/sandbox': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/s3': 0.0.0-codex-native-sidecar-platform-ci.117d645 + '@secure-exec/sandbox': 0.0.0-codex-native-sidecar-platform-ci.117d645 # <<< secure-exec catalog <<< diff --git a/website/docs.config.mjs b/website/docs.config.mjs index 826717148..be38e9c9f 100644 --- a/website/docs.config.mjs +++ b/website/docs.config.mjs @@ -63,6 +63,7 @@ export const siteConfig = { { slug: "docs/agents/codex", label: "Codex", attrs: { "data-icon-src": "/images/registry/codex.svg" }, badge: { text: "Coming Soon", variant: "caution" } }, { slug: "docs/agents/amp", label: "Amp", attrs: { "data-icon-src": "/images/registry/amp.svg" }, badge: { text: "Coming Soon", variant: "caution" } }, { slug: "docs/agents/opencode", label: "OpenCode", attrs: { "data-icon-src": "/images/registry/opencode.svg" }, badge: { text: "Coming Soon", variant: "caution" } }, + { slug: "docs/agents/custom", label: "Custom Agents", attrs: { "data-icon": "wrench" } }, ], }, { slug: "docs/sessions", label: "Sessions & Transcripts", attrs: { "data-icon": "messages" } }, @@ -101,6 +102,14 @@ export const siteConfig = { items: [ { label: "API Reference", link: "/api", attrs: { target: "_blank" } }, { slug: "docs/deployment", label: "Deploy" }, + { + label: "Custom Software", + items: [ + { slug: "docs/custom-software/definition", label: "Definition" }, + { slug: "docs/custom-software/building-wasm", label: "Building Binaries" }, + { label: "Request Software", link: "https://github.com/rivet-dev/agent-os/issues/new/choose", attrs: { target: "_blank" } }, + ], + }, { label: "Architecture", items: [ @@ -127,6 +136,7 @@ export const siteConfig = { label: "More", items: [ { slug: "docs/core", label: "Core SDK" }, + { slug: "docs/debugging", label: "Debugging", attrs: { "data-icon": "bug" } }, { slug: "docs/benchmarks" }, { slug: "docs/cost-evaluation", label: "Cost Evaluation" }, ], diff --git a/website/src/content/docs/docs/agents/custom.mdx b/website/src/content/docs/docs/agents/custom.mdx new file mode 100644 index 000000000..68aece570 --- /dev/null +++ b/website/src/content/docs/docs/agents/custom.mdx @@ -0,0 +1,156 @@ +--- +title: "Custom Agents" +description: "Bring your own coding agent to agentOS by speaking the Agent Client Protocol (ACP) inside the VM." +--- + +import { Aside } from '@astrojs/starlight/components'; + +A custom agent is a program that runs **inside the VM** to drive a coding agent. agentOS spawns it when you call `createSession()` and talks to it over the Agent Client Protocol. You ship it as a software package, exactly like the built-in agents. + +## Agent Client Protocol (ACP) + +agentOS speaks the [Agent Client Protocol (ACP)](https://agentclientprotocol.com) to every agent: JSON-RPC over stdio. The agent reads protocol messages on **stdin** and writes them on **stdout**, so stdout is reserved for ACP and **stderr is used for logs**. Your program only needs to speak ACP; how it runs the underlying model is up to you. See the [ACP documentation](https://agentclientprotocol.com) for the full protocol. + +## Two ways to build an agent + +There are two shapes, depending on whether the agent runs in the ACP process or in its own. + +### Single process (embedded) + +The ACP adapter **embeds the agent SDK** and runs it in the same process. One process inside the VM, lower memory footprint. + + + + + + + + + Host + + ACP + + VM + + ACP adapter + + agent (embedded) + + +For example, an adapter to run **OpenCode**, which speaks ACP natively. One package is both the ACP process and the agent, so there's no separate adapter and nothing else is spawned. + +```ts title="opencode.ts" +import { defineSoftware } from "@rivet-dev/agentos"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +// Example: an adapter to run OpenCode. +export default defineSoftware({ + name: "opencode", + type: "agent", + packageDir, + // A single package provides everything that runs in the VM. + requires: ["@agentos-software/opencode"], + agent: { + id: "opencode", + // Same package for both: OpenCode *is* the ACP process. It speaks ACP + // on stdio itself, so there is no separate adapter to spawn the agent. + acpAdapter: "@agentos-software/opencode", + agentPackage: "@agentos-software/opencode", + staticEnv: { + OPENCODE_DISABLE_CONFIG_DEP_INSTALL: "1", + OPENCODE_DISABLE_EMBEDDED_WEB_UI: "1", + }, + }, +}); +``` + +### ACP adapter (separate agent) + +The ACP adapter is a thin **bridge** that spawns the real agent as its **own process** (a CLI or SDK) and translates between it and ACP. Full agent feature set, higher memory. + + + + + + + + + Host + + ACP + + VM + + ACP adapter + + spawns + + Agent process + (CLI / SDK) + + +For example, an adapter to run **Pi**: the `pi` CLI doesn't speak ACP, so `pi-acp` speaks ACP and spawns the CLI as a separate process. The descriptor names two packages, the adapter and the agent. + +```ts title="pi-cli.ts" +import { defineSoftware } from "@rivet-dev/agentos"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +// Example: a pi-acp adapter that runs the Pi CLI. +export default defineSoftware({ + name: "pi-cli", + type: "agent", + packageDir, + // Two separate packages: the ACP adapter, and the agent it drives. + requires: ["pi-acp", "@mariozechner/pi-coding-agent"], + agent: { + id: "pi-cli", + // `pi-acp` is a thin ACP adapter. It does NOT run the agent itself: + // it speaks ACP on stdio and spawns the `pi` CLI as a separate child + // process, translating between ACP and the CLI. + acpAdapter: "pi-acp", + // The actual agent, launched by pi-acp as its own process. + agentPackage: "@mariozechner/pi-coding-agent", + // Tell the adapter where to find the `pi` CLI inside the VM + // (resolved at boot to a guest path under /root/node_modules). + env: (ctx) => ({ + PI_ACP_PI_COMMAND: ctx.resolveBin("@mariozechner/pi-coding-agent", "pi"), + }), + }, +}); +``` + +## Use your agent + +Register the package on the server with `software`. Sessions are then created from the client by `id`, exactly like any built-in agent. + +```ts title="server.ts" +import { agentOS, setup, defineSoftware } from "@rivet-dev/agentos"; + +const myAgent = defineSoftware({ + type: "agent", + /* ...name, packageDir, requires, agent (see above)... */ +}); + +const vm = agentOS({ software: [myAgent] }); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + +See [Sessions](/docs/sessions) for creating and driving sessions. Ship your adapter as a package so its dependencies resolve from `node_modules/` (via `requires`) inside the VM, rather than as a loose file. + +All built-in agents are defined exactly this way. Browse them for reference on [GitHub](https://github.com/rivet-dev/agent-os/tree/main/registry/agent). + +## Read more + +- [Defining software packages](/docs/custom-software/definition): the full descriptor reference, including every `agent` field (`staticEnv`, `env`, `launchArgs`), the `SoftwareContext` helpers, and the tool and WASM-command software types. +- [Building binaries](/docs/custom-software/building-wasm): compile WASM command binaries and use the registry. + +## Debugging + +When a custom agent exits mid-turn or a tool call fails, capture the agent's stderr with the `onAgentStderr` hook on `AgentOs.create()`. The agent uses stdout for ACP, so stderr carries its logs and crash output. See [Debugging](/docs/debugging) for that hook and the runtime (sidecar) logs. diff --git a/website/src/content/docs/docs/agents/pi.mdx b/website/src/content/docs/docs/agents/pi.mdx index 95519648e..081b13dfe 100644 --- a/website/src/content/docs/docs/agents/pi.mdx +++ b/website/src/content/docs/docs/agents/pi.mdx @@ -79,3 +79,7 @@ const { sessionId } = await agent.createSession("pi", { See the [Pi extension documentation](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent/examples/extensions) for the full extension API. +## Customizing the agent + +Pi is a built-in agent, but it's just a software package under the hood. To ship your own ACP adapter, swap the underlying agent SDK, or register a tweaked Pi build as a new agent, see [Custom Agents](/docs/agents/custom). + diff --git a/website/src/content/docs/docs/architecture.mdx b/website/src/content/docs/docs/architecture.mdx index 6519d4e1b..a8c792c88 100644 --- a/website/src/content/docs/docs/architecture.mdx +++ b/website/src/content/docs/docs/architecture.mdx @@ -297,7 +297,7 @@ An agent (such as [Pi](https://github.com/mariozechner/pi-coding-agent)) is just - **Long-lived.** Where a bare `exec()` runs once and exits, a session keeps an agent alive across many prompts. - **Streamed.** The agent's output flows back to your app in real time as `sessionEvent`s. - **Replayable.** Each session persists a transcript (with sequence numbers) that survives sleep/wake, so you can replay the conversation later. -- **Context injected.** agentOS adds a system prompt describing the VM environment and available tools, layered on top of the agent's own instructions. See [System Prompt](/docs/system-prompt). +- **Context injected.** agentOS adds a system prompt describing the VM environment and available commands and bindings, layered on top of the agent's own instructions. See [System Prompt](/docs/system-prompt). - See [Agent Sessions](/docs/architecture/agent-sessions) for the internals. ### Permissions & approvals diff --git a/website/src/content/docs/docs/bindings.mdx b/website/src/content/docs/docs/bindings.mdx index 7e91018c7..1d64f4a11 100644 --- a/website/src/content/docs/docs/bindings.mdx +++ b/website/src/content/docs/docs/bindings.mdx @@ -89,6 +89,8 @@ When bindings are registered, CLI shims are installed at `/usr/local/bin/agentos The agent interacts with bindings as shell commands: +The listing subcommand is still named `list-tools` for CLI compatibility. + ```bash # List all available bindings groups agentos list-tools diff --git a/website/src/content/docs/docs/custom-software/building-wasm.mdx b/website/src/content/docs/docs/custom-software/building-wasm.mdx new file mode 100644 index 000000000..dca12c905 --- /dev/null +++ b/website/src/content/docs/docs/custom-software/building-wasm.mdx @@ -0,0 +1,86 @@ +--- +title: "Building Binaries" +description: "Compile WASM command binaries for agentOS from source in the secure-exec registry." +--- + +import { Aside } from '@astrojs/starlight/components'; + +WASM command packages (`type: "wasm-commands"`) ship **compiled `.wasm` binaries** that run inside the VM as guest commands. The binaries are build artifacts and are not checked into git, so to add or change a command you build it from source in the **secure-exec registry**. + + + +## Where it lives + +Command source and packages live under `registry/` in [secure-exec](https://github.com/rivet-dev/secure-exec/tree/main/registry): + +- **`registry/native/crates/`**: the Rust source for the WASM commands. +- **`registry/native/c/`**: the C source for the WASM commands. +- **`registry/software//`**: the npm package for each command set (`@agentos-software/`). It contains `dist/` (the TypeScript descriptor) and `wasm/` (the compiled binaries it exposes via `commandDir`). + +## Build + +Build everything from `registry/`: + +```bash +make build # build all WASM binaries + the TypeScript packages +make copy-wasm # copy binaries into each package's wasm/ directory +make test +``` + +`copy-wasm` maps each compiled command into `registry/software//wasm/`, which is the `commandDir` the package exposes. The two toolchains build independently: + +### Rust + +Most commands are Rust. The source lives in `registry/native/crates/` and compiles for `wasm32-wasip1` with the pinned **nightly** toolchain from `rust-toolchain.toml` (the build vendors and patches `std` for WASI). Build just the Rust commands: + +```bash +make build-wasm-rust # runs: cd native && make wasm +``` + +### C + +C-based commands (e.g. `sqlite3`, `unzip`, `wget`, `zip`) live in `registry/native/c/` and compile with a **wasi-sdk** clang toolchain. Build just the C commands: + +```bash +make build-wasm-c # runs: cd native/c && make programs && make install +``` + +## Add a new command package + +1. Add the command source under `registry/native/crates/` (Rust) or `registry/native/c/` (C). +2. Create `registry/software//` as an `@agentos-software/` npm package that exports a descriptor with its `commandDir`. +3. Add a copy rule to the `copy-wasm` target mapping the built binary into `registry/software//wasm/`. +4. If it belongs in a meta-package (e.g. `common` or `build-essential`), add it there. +5. Verify with `make copy-wasm && make build && make test`. + +## Let an agent build it + +This is a mechanical, well-scoped task, so you can hand it to a coding agent. A prompt like: + +```text +Add a WASM command package for `` to the secure-exec registry: +- put the Rust source under registry/native/crates/ (or C under registry/native/c/), +- create registry/software// as an @agentos-software/ npm + package that exports a commandDir descriptor, +- add a copy-wasm rule mapping the built binary into its wasm/ directory, +then run `make copy-wasm && make build && make test` and fix any failures. +``` + +## Using the registry + +Install a published package and pass it to `software`. Registry WASM packages expose a `commandDir`, so you pass them directly (no `defineSoftware()` wrapper): + +```ts title="server.ts" +import { agentOS, setup } from "@rivet-dev/agentos"; +import coreutils from "@agentos-software/coreutils"; +import ripgrep from "@agentos-software/ripgrep"; + +const vm = agentOS({ software: [coreutils, ripgrep] }); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + +Meta-packages bundle a full set, e.g. `@agentos-software/common` (coreutils, sed, grep, gawk, findutils, diffutils, tar, gzip). Run the commands from the client; see [Processes & Shell](/docs/processes). Browse the full catalog on the [Registry](/registry), and see how packages map onto the `wasm-commands` descriptor in [Software Definition](/docs/custom-software/definition#wasm-command-software). diff --git a/website/src/content/docs/docs/custom-software/definition.mdx b/website/src/content/docs/docs/custom-software/definition.mdx new file mode 100644 index 000000000..fad8e1995 --- /dev/null +++ b/website/src/content/docs/docs/custom-software/definition.mdx @@ -0,0 +1,156 @@ +--- +title: "Software Definition" +description: "The full software-package definition for custom agents, command packages, and WASM commands in an agentOS VM." +--- + +import { Aside } from '@astrojs/starlight/components'; + +**Software** is anything you install into a VM: an agent, a command package, or a set of WASM commands. Every package, including the built-ins, is declared with `defineSoftware()` and passed to the `software` option. + +```ts +import { agentOS, setup } from "@rivet-dev/agentos"; +import pi from "@agentos-software/pi"; + +const vm = agentOS({ software: [pi /*, …more */] }); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + +`defineSoftware()` is a typed identity helper. It returns the descriptor unchanged but gives you full type-checking. The descriptor's `type` field discriminates between the three kinds of software. + +## Software types + +- **`"agent"`**: a coding agent runnable via `createSession(id)`. Key fields: `agent`, `requires`, `packageDir`. +- **`"tool"`**: command-package software exposed inside the VM. Key fields: `bins`, `requires`, `packageDir`. +- **`"wasm-commands"`**: compiled WASM command binaries. Key fields: `commandDir`, `aliases`, `permissions`. + +All descriptors share two base fields: + +- **`name`** (required). The software package name. +- **`type`** (required). One of `"agent"`, `"tool"`, or `"wasm-commands"`. + +### Agent software + +Registers a coding agent. See [Custom Agents](/docs/agents/custom) for the agent-focused guide; this is the full field reference. + +- **`packageDir`** (required). This package's directory on the host. Resolves `requires` from its own `node_modules/`, mounted into the VM at `/root/node_modules/`. +- **`requires`** (required). npm packages that must be available inside the VM (must include the adapter and agent packages). +- **`agent.id`** (required). The id passed to `createSession(id)`. Reuse a built-in id to override it, or pick a new one. +- **`agent.acpAdapter`** (required). npm package of the ACP adapter, spawned inside the VM. Must be in `requires`. +- **`agent.agentPackage`** (required). npm package of the underlying agent SDK/CLI. Must be in `requires`. +- **`agent.staticEnv`** (optional). Static env vars passed when spawning the adapter (merged **under** user `env`). +- **`agent.env`** (optional). `(ctx: SoftwareContext) => Record`; env computed at boot, e.g. to resolve a bin path. +- **`agent.launchArgs`** (optional). Extra CLI args prepended when launching the adapter. + +```ts title="pi.ts" +import { defineSoftware } from "@rivet-dev/agentos"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +export default defineSoftware({ + name: "pi", + type: "agent", + packageDir, + requires: ["@agentos-software/pi", "@mariozechner/pi-coding-agent"], + agent: { + id: "pi", + acpAdapter: "@agentos-software/pi", + agentPackage: "@mariozechner/pi-coding-agent", + }, +}); +``` + +### Command package software + +Exposes one or more CLI commands inside the VM by mapping a command name to the npm package that provides its `bin`. The descriptor value is still `"tool"` because this is the software package type, separate from host [bindings](/docs/bindings). + +- **`packageDir`** (required). This package's directory on the host (resolves `requires`). +- **`requires`** (required). npm packages that must be available inside the VM. +- **`bins`** (required). `Record`, mapping the command name as invoked in the VM to the npm package providing it. + +```ts title="my-tool.ts" +import { defineSoftware } from "@rivet-dev/agentos"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +export default defineSoftware({ + name: "my-tool", + type: "tool", + packageDir, + requires: ["@my-org/my-cli"], + bins: { "my-cli": "@my-org/my-cli" }, +}); +``` + +### WASM command software + +Registers compiled WebAssembly command binaries (coreutils, ripgrep, jq, …). See [Building Binaries](/docs/custom-software/building-wasm) for how to produce the binaries. + +- **`commandDir`** (required). Absolute host path to the directory of `.wasm` command binaries. +- **`aliases`** (optional). `Record`; symlink-style aliases. +- **`permissions`** (optional). Permission-tier assignments: `full`, `readWrite`, `readOnly` (`string[]` or `"*"`), `isolated`. + +```ts title="my-commands.ts" +import { defineSoftware } from "@rivet-dev/agentos"; + +export default defineSoftware({ + name: "my-commands", + type: "wasm-commands", + commandDir: "/abs/path/to/wasm", + aliases: { ll: "ls" }, + permissions: { readOnly: "*" }, +}); +``` + + + +## `SoftwareContext` + +The `agent.env` callback (and other dynamic config) receives a `SoftwareContext` for resolving VM-side paths: + +```ts +interface SoftwareContext { + // Resolve a package's bin to its VM path, e.g. + // ctx.resolveBin("@mariozechner/pi-coding-agent", "pi") + // -> "/root/node_modules/@mariozechner/pi-coding-agent/dist/cli.js" + resolveBin(packageName: string, binName?: string): string; + + // Resolve a package root to its VM path, e.g. + // ctx.resolvePackage("pi-acp") -> "/root/node_modules/pi-acp" + resolvePackage(packageName: string): string; +} +``` + +```ts +agent: { + id: "pi-cli", + acpAdapter: "pi-acp", + agentPackage: "@mariozechner/pi-coding-agent", + env: (ctx) => ({ + PI_ACP_PI_COMMAND: ctx.resolveBin("@mariozechner/pi-coding-agent", "pi"), + }), +} +``` + +## Meta-packages + +A software entry may be an **array** of descriptors, letting one package bundle several (e.g. a "build-essential" set). Pass arrays directly to `software`: + +```ts +const vm = agentOS({ + software: [pi, buildEssential /* = [coreutils, make, git, curl] */], +}); +``` + +## Next steps + +- [Custom Agents](/docs/agents/custom): the agent-focused guide. +- [Building Binaries](/docs/custom-software/building-wasm): compile WASM commands and use the registry. +- [Request Software](https://github.com/rivet-dev/agent-os/issues/new/choose): ask for a package you need. diff --git a/website/src/content/docs/docs/debugging.mdx b/website/src/content/docs/docs/debugging.mdx new file mode 100644 index 000000000..cc3a4f467 --- /dev/null +++ b/website/src/content/docs/docs/debugging.mdx @@ -0,0 +1,54 @@ +--- +title: "Debugging" +description: "Capture agent logs and runtime (sidecar) logs to diagnose sessions, tool calls, and crashes." +--- + +import { Aside } from '@astrojs/starlight/components'; + +Two log streams help diagnose what's happening inside a VM: the **agent's** own output and the **runtime (sidecar)** logs. + +## Agent logs (`onAgentStderr`) + +The coding agent (ACP adapter) runs as a process inside the VM and uses **stdout for the ACP protocol**, so its **stderr** carries the agent's logs, warnings, and crash output — the first place to look when a tool call or session fails mid-turn. Capture it with `onAgentStderr` on the VM: + +```ts +const agentOs = await AgentOs.create({ + software: [pi], + onAgentStderr(event) { + // event: { sessionId, agentType, processId, pid, chunk: Uint8Array } + process.stderr.write(`[agent:${event.agentType}] `); + process.stderr.write(event.chunk); + }, +}); +``` + +It's a VM-level option covering every session's agent process; if omitted, chunks are written to the host `process.stderr` by default. See [Sessions → Agent logs](/docs/sessions#agent-logs). + +## Runtime logs (sidecar) + +The agentOS sidecar emits structured **logfmt** logs for request handling, networking, and lifecycle. Configure them with environment variables on the **host process** (the sidecar inherits the host environment): + +| env var | effect | +|---------|--------| +| `AGENTOS_LOG_LEVEL` / `LOG_LEVEL` / `RUST_LOG` | log filter, in that priority. Uses [EnvFilter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) syntax, e.g. `debug`, `info`, `agentos_sidecar=debug,info`. Default `info`. | +| `RUST_LOG_FORMAT` | `logfmt` (default) or `text` | +| `AGENTOS_LOG_FILE` | append logs to this file instead of stderr (never stdout, which carries the wire protocol) | +| `RUST_LOG_{SPAN_NAME,SPAN_PATH,TARGET,LOCATION,MODULE_PATH,ANSI_COLOR}` | per-field toggles (`=1` to enable) | + +```bash +AGENTOS_LOG_LEVEL=debug AGENTOS_LOG_FILE=./sidecar.log RUST_LOG_FORMAT=logfmt node app.mjs +``` + +Produces logfmt lines such as: + +```text +ts=2026-… level=info message="ext request received" kind=create_session +ts=2026-… level=info message="ext request handled" kind=create_session elapsed_ms=1798 +ts=2026-… level=debug message="querying: api.anthropic.com. A" +``` + + + +Use **agent logs** to see what the agent did (tool calls, model errors), and **runtime logs** to see what the sidecar did around it (request timing, DNS, lifecycle). diff --git a/website/src/content/docs/docs/sessions.mdx b/website/src/content/docs/docs/sessions.mdx index 1c70d4bd3..12dc80854 100644 --- a/website/src/content/docs/docs/sessions.mdx +++ b/website/src/content/docs/docs/sessions.mdx @@ -371,3 +371,28 @@ registry.start(); *[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)* +## Agent logs + +The agent (ACP adapter) runs as a process inside the VM. It uses **stdout** for ACP protocol traffic, so its **stderr** is the channel for logs, warnings, and crash diagnostics. Pass `onAgentStderr` to the VM to capture it, and route it to your own logger to see exactly what the agent is doing (or why it exited). + + + +```ts title="server.ts" +import { agentOS, setup } from "@rivet-dev/agentos"; +import pi from "@agentos-software/pi"; + +const vm = agentOS({ + software: [pi], + onAgentStderr(event) { + // event: { sessionId, agentType, processId, pid, chunk: Uint8Array } + const line = new TextDecoder().decode(event.chunk); + logger.info(`[agent:${event.agentType} session:${event.sessionId}] ${line}`); + }, +}); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + diff --git a/website/src/content/docs/docs/software.mdx b/website/src/content/docs/docs/software.mdx index 06072aae2..0cb1487de 100644 --- a/website/src/content/docs/docs/software.mdx +++ b/website/src/content/docs/docs/software.mdx @@ -62,6 +62,6 @@ console.log(result.stdout); Browse all available software packages on the [Registry](/registry). -## Publishing Custom Packages +## Custom Software -See the [agentos-registry contributing guide](https://github.com/rivet-dev/agent-os/blob/main/registry/CONTRIBUTING.md) for how to add new software packages to the registry. +Package your own agents, command packages, and WASM commands. See [Software Definition](/docs/custom-software/definition) to define a package, and [Building Binaries](/docs/custom-software/building-wasm) to compile WASM commands from source in the [secure-exec registry](https://github.com/rivet-dev/secure-exec/tree/main/registry). diff --git a/website/src/content/docs/docs/system-prompt.mdx b/website/src/content/docs/docs/system-prompt.mdx index 2ec54c1b4..b73f29bf4 100644 --- a/website/src/content/docs/docs/system-prompt.mdx +++ b/website/src/content/docs/docs/system-prompt.mdx @@ -4,21 +4,21 @@ description: "How agentOS injects context into agent sessions." skill: true --- -agentOS automatically injects a system prompt into every agent session that describes the VM environment and available tools. The prompt is additive and never replaces the agent's own instructions (CLAUDE.md, AGENTS.md, etc.). +agentOS automatically injects a system prompt into every agent session that describes the VM environment and available commands and bindings. The prompt is additive and never replaces the agent's own instructions (CLAUDE.md, AGENTS.md, etc.). -The base prompt is embedded in the sidecar (not written to a file inside the VM). At session start the sidecar assembles the base prompt with your additional instructions and generated tool docs, then injects the result into the agent adapter's launch arguments (for example, `--append-system-prompt` for Pi). +The base prompt is embedded in the sidecar (not written to a file inside the VM). At session start the sidecar assembles the base prompt with your additional instructions and generated binding docs, then injects the result into the agent adapter's launch arguments (for example, `--append-system-prompt` for Pi). ## Customization -- `additionalInstructions` appends extra instructions to the agent's system prompt. They are added after the base OS prompt and the generated tool docs, so they layer on top of (rather than replace) the agent's own instructions. -- `skipOsInstructions` suppresses the base OS prompt while still injecting the generated tool docs. +- `additionalInstructions` appends extra instructions to the agent's system prompt. They are added after the base OS prompt and the generated binding docs, so they layer on top of (rather than replace) the agent's own instructions. +- `skipOsInstructions` suppresses the base OS prompt while still injecting the generated binding docs. ```ts const session = await vm.createSession("pi", { env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, // Extra instructions appended to the agent system prompt additionalInstructions: "Always write tests before implementation.", - // Suppress the base OS prompt (tool docs are still injected) + // Suppress the base OS prompt (binding docs are still injected) skipOsInstructions: true, }); ```