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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ the guest — over inventing a softer fallback that hides the failure.
they do not scan `node_modules` or parse adapter manifests for discovery.
- TypeScript and Rust clients must stay behaviorally identical. Any public
method or wire behavior change in one client must be mirrored in the other.
- Clients are thin transport adapters, not runtime policy owners. They may
validate and serialize explicit caller input, forward requests, route host
callbacks/events, and retain host-only state that the sidecar cannot access.
VM defaults, base environment, filesystem/bootstrap policy, default software,
permission policy, agent/session orchestration, prompt assembly, and other
behavior shared across clients belong in the sidecar/runtime.
- Behavioral parity must come from one sidecar-owned implementation, not copied
TypeScript/Rust/actor constants or parallel state machines. Prefer omitted
wire fields meaning "use the sidecar default"; clients should send overrides
only when the caller explicitly supplied them.
- Agent adapters must use real upstream SDKs. Do not replace SDK adapters with
direct API-call stubs.

Expand Down
5 changes: 2 additions & 3 deletions crates/agentos-actor-plugin/src/actions/contract_surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,8 @@ pub fn render_actor_actions_ts() -> String {

let mut out = String::new();
out.push_str("// @generated by agentos-actor-plugin. Do not edit.\n");
out.push_str("// This file is generated locally and is intentionally not committed to Git.\n");
out.push_str("// Regenerated by the agentos-actor-plugin build script; build the crate\n");
out.push_str("// (e.g. `cargo build -p agentos-actor-plugin`) to refresh it.\n");
out.push_str("// This file is committed so package builds do not need to compile the native\n");
out.push_str("// plugin just to regenerate TypeScript action types.\n");

for import in TYPE_IMPORTS {
render_import(&mut out, import);
Expand Down
28 changes: 25 additions & 3 deletions crates/execution/src/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ const WASM_SIDECAR_ROUTED_FS_SYNC_METHODS: &[&str] = &[
"fs.writeFileSync",
"fs.writeSync",
];
const WASM_SIDECAR_ROUTED_KERNEL_SYNC_METHODS: &[&str] = &[
"__kernel_isatty",
"__kernel_poll",
"__kernel_stdin_read",
"__kernel_stdio_write",
"__kernel_tty_size",
"__pty_set_raw_mode",
];

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WasmSignalDispositionAction {
Expand Down Expand Up @@ -1525,7 +1533,8 @@ fn wasm_sync_rpc_method_routes_through_sidecar_kernel(
internal_sync_rpc: &WasmInternalSyncRpc,
) -> bool {
internal_sync_rpc.route_fs_through_sidecar
&& WASM_SIDECAR_ROUTED_FS_SYNC_METHODS.contains(&request.method.as_str())
&& (WASM_SIDECAR_ROUTED_FS_SYNC_METHODS.contains(&request.method.as_str())
|| WASM_SIDECAR_ROUTED_KERNEL_SYNC_METHODS.contains(&request.method.as_str()))
}

fn translate_wasm_guest_path(
Expand Down Expand Up @@ -3842,7 +3851,8 @@ mod tests {
DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB, NODE_WASI_MODULE_SOURCE,
WASM_CAPTURED_OUTPUT_LIMIT_BYTES, WASM_MAX_FUEL_ENV, WASM_MAX_MEMORY_BYTES_ENV,
WASM_MAX_STACK_BYTES_ENV, WASM_PAGE_BYTES, WASM_SANDBOX_ROOT_ENV,
WASM_SIDECAR_ROUTED_FS_SYNC_METHODS, WASM_SYNC_READ_LIMIT_BYTES,
WASM_SIDECAR_ROUTED_FS_SYNC_METHODS, WASM_SIDECAR_ROUTED_KERNEL_SYNC_METHODS,
WASM_SYNC_READ_LIMIT_BYTES,
};
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::fs;
Expand Down Expand Up @@ -4620,7 +4630,7 @@ mod tests {
}

#[test]
fn wasm_sidecar_managed_fs_methods_route_to_kernel_sync_rpc() {
fn wasm_sidecar_managed_methods_route_to_kernel_sync_rpc() {
let mut standalone = WasmInternalSyncRpc {
module_guest_paths: Vec::new(),
module_host_path: PathBuf::from("/tmp/module.wasm"),
Expand Down Expand Up @@ -4658,6 +4668,18 @@ mod tests {
);
}

for method in WASM_SIDECAR_ROUTED_KERNEL_SYNC_METHODS {
let request = wasm_sync_rpc_request(method);
assert!(
wasm_sync_rpc_method_routes_through_sidecar_kernel(&request, &sidecar_managed),
"{method} should route through the sidecar kernel for managed WASI executions"
);
assert!(
!wasm_sync_rpc_method_routes_through_sidecar_kernel(&request, &standalone),
"{method} should stay local for standalone/prewarm WASI execution"
);
}

standalone.route_fs_through_sidecar = true;
let non_fs_request = wasm_sync_rpc_request("child_process.spawn");
assert!(!wasm_sync_rpc_method_routes_through_sidecar_kernel(
Expand Down
81 changes: 80 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,86 @@ install-shell:
(cd packages/shell && PATH="$global_bin_dir:$PATH" pnpm link --global)

shell *args:
NODE_OPTIONS="--no-deprecation ${NODE_OPTIONS:-}" pnpm --filter @rivet-dev/agentos-shell exec tsx src/main.ts -i -t "$@"
#!/usr/bin/env bash
set -euo pipefail
actor_mode=false
for arg in "$@"; do
if [[ "$arg" == "--actor" ]]; then
actor_mode=true
fi
done
if [[ ! -x packages/shell/node_modules/.bin/tsx \
|| ! -e packages/shell/node_modules/@agentos-software/codex-cli \
|| ! -d packages/build-tools/node_modules ]]; then
pnpm install --force
fi
missing_registry_packages=()
for package_json in packages/shell/node_modules/@agentos-software/*/package.json; do
IFS=$'\t' read -r package_name package_main < <(node -e '
const manifest = require(require("node:path").resolve(process.argv[1]));
console.log(`${manifest.name}\t${manifest.main ?? ""}`);
' "$package_json")
package_dir="${package_json%/package.json}"
if [[ -n "$package_main" && ( ! -e "$package_dir/${package_main#./}" \
|| ! -e "$package_dir/dist/package.aospkg" ) ]]; then
missing_registry_packages+=("$package_name")
fi
done
if (( ${#missing_registry_packages[@]} > 0 )); then
pnpm --filter @agentos-software/manifest build
pnpm --filter @rivet-dev/agentos-toolchain build
registry_filters=()
for package_name in "${missing_registry_packages[@]}"; do
registry_filters+=(--filter "$package_name")
done
pnpm "${registry_filters[@]}" build
fi
if [[ ! -e registry/software/common/dist/index.js ]]; then
pnpm --filter @agentos-software/common build
fi
if [[ ! -e packages/runtime-core/dist/index.js \
|| ! -e packages/core/dist/index.js \
|| ! -e packages/agentos/dist/index.js ]]; then
pnpm --filter @rivet-dev/agentos-runtime-core build
pnpm --filter @rivet-dev/agentos-core build
pnpm --filter @rivet-dev/agentos build
fi
if [[ "$actor_mode" == true ]]; then
r6_root="${AGENTOS_R6_ROOT:-$PWD/../r6}"
rivetkit_loader="$r6_root/rivetkit-typescript/packages/rivetkit/node_modules/tsx/dist/loader.mjs"
if [[ ! -e "$r6_root/pnpm-lock.yaml" ]]; then
echo "just shell --actor requires the Rivet repo at $r6_root (override with AGENTOS_R6_ROOT)" >&2
exit 1
fi
if [[ ! -e "$rivetkit_loader" ]]; then
pnpm --dir "$r6_root" install --frozen-lockfile --filter 'rivetkit...'
fi
if [[ ! -e "$r6_root/shared/typescript/virtual-websocket/dist/mod.js" \
|| ! -e "$r6_root/rivetkit-typescript/packages/traces/dist/tsup/index.js" \
|| ! -e "$r6_root/rivetkit-typescript/packages/workflow-engine/dist/tsup/index.js" \
|| ! -e "$r6_root/engine/sdks/typescript/envoy-protocol/dist/index.js" \
|| ! -e "$r6_root/rivetkit-typescript/packages/rivetkit-wasm/pkg/rivetkit_wasm.js" ]]; then
pnpm --dir "$r6_root" --filter 'rivetkit...' build
fi
fi
cargo_packages=(-p agentos-sidecar)
if [[ "$actor_mode" == true ]]; then
cargo_packages+=(-p agentos-actor-plugin)
fi
CARGO_TARGET_DIR="$PWD/target" cargo build "${cargo_packages[@]}"
actor_env=()
if [[ "$actor_mode" == true ]]; then
case "$(uname -s)" in
Darwin) plugin_name=libagentos_actor_plugin.dylib ;;
Linux) plugin_name=libagentos_actor_plugin.so ;;
*) plugin_name=agentos_actor_plugin.dll ;;
esac
actor_env+=(AGENTOS_PLUGIN_BIN="$PWD/target/debug/$plugin_name")
fi
env "${actor_env[@]}" \
AGENTOS_SIDECAR_BIN="$PWD/target/debug/agentos-sidecar" \
NODE_OPTIONS="--no-deprecation ${NODE_OPTIONS:-}" \
pnpm --filter @rivet-dev/agentos-shell exec tsx src/main.ts "$@"

# Run the agentos-sdk.dev site (landing + /docs) locally with hot reload
docs:
Expand Down
14 changes: 7 additions & 7 deletions packages/core/tests/brush-interactive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ import type { AgentOs } from "../src/index.js";

const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, "../../..");
const SIDECAR_BINARY = resolve(REPO_ROOT, "target/debug/agentos-sidecar");
const SIDECAR_BINARY =
process.env.AGENTOS_SIDECAR_BIN ??
resolve(REPO_ROOT, "target/debug/agentos-sidecar");
const REGISTRY_SH_CANDIDATES = [
"../secure-exec/registry/native/target/wasm32-wasip1/release/commands/sh",
"../secure-exec-provides/registry/native/target/wasm32-wasip1/release/commands/sh",
"packages/runtime-core/commands/sh",
"registry/native/target/wasm32-wasip1/release/commands/sh",
].map((candidate) => resolve(REPO_ROOT, candidate));
const REGISTRY_SH = REGISTRY_SH_CANDIDATES.find((candidate) =>
existsSync(candidate),
Expand Down Expand Up @@ -72,10 +74,8 @@ async function waitFor(term: Terminal, text: string, timeoutMs = 20000): Promise
throw new Error(`timeout waiting for ${JSON.stringify(text)}\n${snapshot("timeout", term)}`);
}

// Requires the `sh` wasm command built locally (`make` in the secure-exec
// sibling's registry/native). CI consumes published @agentos-software packages
// and does not build wasm commands, so skip when the artifact is absent rather
// than failing the suite.
// Requires the vendored or locally-built `sh` wasm command. Skip when the
// artifact is absent rather than failing suites that do not build WASM commands.
describe.skipIf(REGISTRY_SH === undefined)("brush interactive PTY repaint", () => {
let vm: AgentOs | undefined;
let term: Terminal | undefined;
Expand Down
6 changes: 5 additions & 1 deletion packages/shell/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# agentos-shell

- Run the local interactive shell from the repo root with `just shell`. If workspace dependency links are missing, the recipe repairs the partially installed workspace with `pnpm install --force`. It also rebuilds missing shell software and direct runtime entrypoints, then visibly builds the sidecar at `target/debug/agentos-sidecar` before opening the guest prompt. Cold registry and sidecar builds can take a while; later runs are incremental.
- Keep `just shell` a transparent shim over the shell CLI: forward its arguments without injecting CLI flags. The CLI itself defaults stdin attachment and TTY mode on; callers can opt out with `--no-interactive` or `--no-tty`.
- `just shell` always uses the in-repo AgentOS sidecar it just built, even if the host environment exports `AGENTOS_SIDECAR_BIN`. To deliberately test another binary, invoke the shell CLI directly with that override.
- `just shell --actor` additionally builds and pins the in-repo actor plugin. Actor development requires the Rivet repo at `../r6` (override with `AGENTOS_R6_ROOT`); the recipe installs the filtered `rivetkit` dependency graph there when its `tsx` loader is missing.
- Do not implement or route through a custom/synthetic shell, prompt, line editor, or command parser; interactive shell mode must launch native Bash through the terminal/PTY path so behavior matches `docker run -it bash`.
- Keep `agentos-shell` loading every command-providing package from secure-exec `registry/software/`; when that registry changes, update the imports, package dependencies, and smoke coverage here.
- Keep `agentos-shell` loading every command-providing package from the in-repo `registry/software/`; when that registry changes, update the imports, package dependencies, and smoke coverage here.
2 changes: 2 additions & 0 deletions packages/shell/src/actor-vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const POOL_NAME = "agentos-shell";
export interface ActorShellVmOptions {
software: SoftwareInput[];
mounts: MountConfig[];
defaultSoftware?: boolean;
limits?: unknown;
}

Expand Down Expand Up @@ -256,6 +257,7 @@ export async function createActorShellVm(
AGENTOS_SHELL_ACTOR_OPTIONS: JSON.stringify({
software: options.software,
mounts: options.mounts,
defaultSoftware: options.defaultSoftware,
...(options.limits ? { limits: options.limits } : {}),
}),
AGENTOS_R6_ROOT: r6,
Expand Down
12 changes: 8 additions & 4 deletions packages/shell/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* Goal: `agentos-shell` should feel like the VM equivalent of `docker run`.
*
* Keep the CLI surface intentionally close to Docker's process flags:
* `-i/--interactive` keeps stdin attached, `-t/--tty` connects a terminal,
* stdin and TTY mode default on; `--no-interactive` and `--no-tty` disable them.
* `-i/--interactive` and `-t/--tty` remain accepted for Docker CLI familiarity,
* `-e/--env` and `--env-file` inject environment variables, `-v/--volume`
* and `--mount type=bind,...` mount host paths, and `-w/--workdir` chooses
* the guest cwd. When TTY mode is requested, the guest command goes through
Expand Down Expand Up @@ -239,9 +240,11 @@ function parseCli(argv: string[]): CliOptions {
.argument("[command]", "guest command to run", "bash")
.argument("[args...]", "guest command arguments")
.addOption(
new Option("-i, --interactive", "keep stdin attached").default(false),
new Option("-i, --interactive", "keep stdin attached").default(true),
)
.addOption(new Option("-t, --tty", "connect a terminal").default(false))
.addOption(new Option("--no-interactive", "detach stdin"))
.addOption(new Option("-t, --tty", "connect a terminal").default(true))
.addOption(new Option("--no-tty", "disable terminal mode"))
.addOption(
new Option(
"--actor",
Expand Down Expand Up @@ -688,11 +691,12 @@ const env = buildEnv(cli);
const mounts = buildMounts(cli);

const vm: ShellVmHandle = cli.actor
? await createActorShellVm({ software, mounts })
? await createActorShellVm({ software, mounts, defaultSoftware: false })
: await AgentOs.create({
mounts,
permissions: allowAll,
software,
defaultSoftware: false,
});

let exitCode = 1;
Expand Down
18 changes: 15 additions & 3 deletions packages/shell/tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ describe("agentos-shell cli", () => {
expect(result.status).toBe(0);
expect(result.stdout).toContain("agentos-shell");
expect(result.stdout).toContain("-i, --interactive");
expect(result.stdout).toContain("--no-interactive");
expect(result.stdout).toContain("-t, --tty");
expect(result.stdout).toContain("--no-tty");
expect(result.stdout).toContain("-e, --env <env>");
expect(result.stdout).toContain("-v, --volume <spec>");
expect(result.stderr).not.toContain("agent-os shell");
Expand Down Expand Up @@ -62,13 +64,23 @@ describe("agentos-shell cli", () => {
expect(result.stdout).toContain("mounted");
});

test("keeps stdin attached when -i is set", () => {
const result = runCli(["--interactive", "--", "bash"], "echo hello\n");
test("keeps stdin attached by default", () => {
const result = runCli(["--", "bash"], "echo hello\n");

expect(result.status).toBe(0);
expect(result.stdout).toContain("hello");
});

test("detaches stdin when --no-interactive is set", () => {
const result = runCli(
["--no-interactive", "--", "bash"],
"echo should-not-run\n",
);

expect(result.status).toBe(0);
expect(result.stdout).not.toContain("should-not-run");
});

test("runs a command through terminal mode when -t is set", () => {
const result = runCli([
"--workdir",
Expand All @@ -85,7 +97,7 @@ describe("agentos-shell cli", () => {
});

test("default terminal mode launches bash instead of the synthetic shell", () => {
const result = runCli(["--interactive", "--tty"], "echo $0\nexit\n");
const result = runCli([], "echo $0\nexit\n");

expect(result.status).toBe(0);
expect(result.stdout).toContain("bash");
Expand Down