Skip to content
Open
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
4 changes: 4 additions & 0 deletions .agents/resume
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail

# This repository has no long-lived services to repair when an orb resumes.
55 changes: 55 additions & 0 deletions .agents/setup
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
node_version="22.19.0"
node_dir="$HOME/.local/share/node-v$node_version"

if [[ ! -x "$node_dir/bin/node" ]]; then
archive="node-v$node_version-linux-x64.tar.xz"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
curl -fsSLo "$tmp_dir/$archive" "https://nodejs.org/dist/v$node_version/$archive"
curl -fsSLo "$tmp_dir/SHASUMS256.txt" "https://nodejs.org/dist/v$node_version/SHASUMS256.txt"
(
cd "$tmp_dir"
grep " $archive$" SHASUMS256.txt | sha256sum --check --strict
)
mkdir -p "$(dirname "$node_dir")"
tar -xJf "$tmp_dir/$archive" -C "$(dirname "$node_dir")"
mv "$(dirname "$node_dir")/node-v$node_version-linux-x64" "$node_dir"
fi

mkdir -p "$HOME/.local/bin"
for command in node npm npx corepack; do
ln -sfn "$node_dir/bin/$command" "$HOME/.local/bin/$command"
done

"$node_dir/bin/corepack" prepare pnpm@10.13.1 --activate
for command in pnpm pnpx; do
ln -sfn "$node_dir/bin/$command" "$HOME/.local/bin/$command"
done

if [[ ! -x "$HOME/.cargo/bin/cargo" ]]; then
curl --proto '=https' --tlsv1.2 -fsS https://sh.rustup.rs |
sh -s -- -y --profile minimal --default-toolchain stable
fi
for command in cargo rustc rustfmt rustup; do
ln -sfn "$HOME/.cargo/bin/$command" "$HOME/.local/bin/$command"
done

cd "$repo_root"
if [[ ! -f website/vendor/theme/package.json ]]; then
theme_dir="$(mktemp -d)"
git -C "$theme_dir" init --quiet
git -C "$theme_dir" remote add origin https://github.com/rivet-dev/docs-theme.git
git -C "$theme_dir" fetch --quiet --depth=1 origin 450c498555135098c6a927adfdf13458be9be22a
git -C "$theme_dir" checkout --quiet FETCH_HEAD
mkdir -p website/vendor
rm -rf website/vendor/theme
cp -R "$theme_dir/packages/theme" website/vendor/theme
rm -rf "$theme_dir"
fi

pnpm install --frozen-lockfile
pnpm --dir website/vendor/theme build
1 change: 1 addition & 0 deletions crates/execution/src/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6871,6 +6871,7 @@ export const chmod = _m.chmod;
export const chown = _m.chown;
export const copyFile = _m.copyFile;
export const cp = _m.cp;
export const glob = _m.glob;
export const lchmod = _m.lchmod;
export const lchown = _m.lchown;
export const link = _m.link;
Expand Down
5 changes: 4 additions & 1 deletion crates/execution/tests/javascript_v8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1790,14 +1790,17 @@ fn javascript_execution_imports_node_fs_promises_without_hanging() {
wasm_module_bytes: None,
inline_code: Some(String::from(
r#"
import fs from "node:fs/promises";
import fs, { glob } from "node:fs/promises";

if (typeof fs.access !== "function") {
throw new Error("node:fs/promises did not expose access()");
}
if (typeof fs.readFile !== "function") {
throw new Error("node:fs/promises did not expose readFile()");
}
if (typeof fs.glob !== "function" || glob !== fs.glob) {
throw new Error("node:fs/promises did not expose glob()");
}
"#,
)),
})
Expand Down
22 changes: 22 additions & 0 deletions crates/v8-runtime/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,28 @@ pub fn install_high_resolution_time_global(scope: &mut v8::HandleScope, origin:
global.define_own_property(scope, key.into(), func.into(), attr);
}

pub fn install_process_exit_global(scope: &mut v8::HandleScope) {
let context = scope.get_current_context();
let global = context.global(scope);
let template = v8::FunctionTemplate::builder(process_exit_callback).build(scope);
let Some(function) = template.get_function(scope) else {
return;
};
let key = v8::String::new(scope, "__secureExecProcessExit").unwrap();
let attributes = v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_DELETE;
global.define_own_property(scope, key.into(), function.into(), attributes);
}

fn process_exit_callback(
scope: &mut v8::HandleScope,
args: v8::FunctionCallbackArguments,
_rv: v8::ReturnValue,
) {
let code = args.get(0).int32_value(scope).unwrap_or(0);
crate::isolate::set_process_exit(scope, code);
scope.terminate_execution();
}

pub fn install_require_esm_sync_global(scope: &mut v8::HandleScope) {
let context = scope.get_current_context();
let global = context.global(scope);
Expand Down
26 changes: 26 additions & 0 deletions crates/v8-runtime/src/isolate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,31 @@ pub struct PromiseRejectState {
overflow_count: usize,
}

#[derive(Default)]
struct ProcessExitState {
code: Option<i32>,
}

pub fn clear_process_exit(isolate: &mut v8::Isolate) {
isolate
.get_slot_mut::<ProcessExitState>()
.expect("process exit state must be installed")
.code = None;
}

pub fn set_process_exit(isolate: &mut v8::Isolate, code: i32) {
isolate
.get_slot_mut::<ProcessExitState>()
.expect("process exit state must be installed")
.code = Some(code);
}

pub fn process_exit_code(isolate: &v8::Isolate) -> Option<i32> {
isolate
.get_slot::<ProcessExitState>()
.and_then(|state| state.code)
}

impl PromiseRejectState {
fn record_unhandled(&mut self, promise_id: i32, error: ExecutionError) {
use std::collections::hash_map::Entry;
Expand Down Expand Up @@ -105,6 +130,7 @@ extern "C" fn promise_reject_callback(msg: v8::PromiseRejectMessage) {

pub fn configure_isolate(isolate: &mut v8::OwnedIsolate) {
isolate.set_slot(PromiseRejectState::default());
isolate.set_slot(ProcessExitState::default());
isolate.set_promise_reject_callback(promise_reject_callback);
}

Expand Down
89 changes: 68 additions & 21 deletions crates/v8-runtime/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2489,13 +2489,21 @@ fn session_thread(

let iso = v8_isolate.as_mut().unwrap();
iso.cancel_terminate_execution();
isolate::clear_process_exit(iso);

// Create execution context: Context::new on a snapshot-restored
// isolate gives a fresh clone of the snapshot's default context
// (bridge IIFE already executed, all infrastructure set up).
// On a non-snapshot isolate, this gives a blank context.
let exec_context = isolate::create_context(iso);

{
let scope = &mut v8::HandleScope::new(iso);
let ctx = v8::Local::new(scope, &exec_context);
let scope = &mut v8::ContextScope::new(scope, ctx);
execution::install_process_exit_global(scope);
}

if high_resolution_time {
let scope = &mut v8::HandleScope::new(iso);
let ctx = v8::Local::new(scope, &exec_context);
Expand Down Expand Up @@ -2802,11 +2810,16 @@ fn session_thread(
&mut bridge_cache,
)
};
let mut process_exit_code = isolate::process_exit_code(iso);
if process_exit_code.is_some() {
exports = None;
error = None;
}

// Re-check async ESM completion once immediately so
// pure-microtask top-level await settles without
// needing a bridge event-loop round-trip.
if mode != 0 && error.is_none() {
if mode != 0 && error.is_none() && process_exit_code.is_none() {
let scope = &mut v8::HandleScope::new(iso);
let ctx = v8::Local::new(scope, &exec_context);
let scope = &mut v8::ContextScope::new(scope, ctx);
Expand All @@ -2826,9 +2839,10 @@ fn session_thread(
// are visible yet — the module body may have registered
// timers, stdin listeners, or child_process handles that
// need event loop pumping to deliver their callbacks.
let should_enter_event_loop = !pending.is_empty()
|| execution::has_pending_module_evaluation()
|| execution::has_pending_script_evaluation();
let should_enter_event_loop = process_exit_code.is_none()
&& (!pending.is_empty()
|| execution::has_pending_module_evaluation()
|| execution::has_pending_script_evaluation());
let event_loop_status = if should_enter_event_loop {
let scope = &mut v8::HandleScope::new(iso);
let ctx = v8::Local::new(scope, &exec_context);
Expand All @@ -2849,8 +2863,14 @@ fn session_thread(
EventLoopStatus::Completed
};

let mut terminated =
matches!(event_loop_status, EventLoopStatus::Terminated);
process_exit_code =
process_exit_code.or_else(|| isolate::process_exit_code(iso));
if process_exit_code.is_some() {
exports = None;
error = None;
}
let mut terminated = process_exit_code.is_some()
|| matches!(event_loop_status, EventLoopStatus::Terminated);
if let EventLoopStatus::Failed(next_code, next_error) = event_loop_status {
code = next_code;
error = Some(next_error);
Expand Down Expand Up @@ -2918,21 +2938,23 @@ fn session_thread(
// Phase 2: pump the event loop for that quiescence wait.
if !pending.is_empty() || execution::has_pending_script_evaluation()
{
let scope = &mut v8::HandleScope::new(iso);
let ctx = v8::Local::new(scope, &exec_context);
let scope = &mut v8::ContextScope::new(scope, ctx);
let event_loop_status = run_event_loop_with_readiness(
scope,
EventLoopSources {
commands: &rx,
readiness: &ready_broker,
readiness_wakes: &ready_rx,
bridge_responses: Some(&async_response_rx),
abort: Some(&abort_rx),
pause: Some(&pause_control),
},
&pending,
);
let event_loop_status = {
let scope = &mut v8::HandleScope::new(iso);
let ctx = v8::Local::new(scope, &exec_context);
let scope = &mut v8::ContextScope::new(scope, ctx);
run_event_loop_with_readiness(
scope,
EventLoopSources {
commands: &rx,
readiness: &ready_broker,
readiness_wakes: &ready_rx,
bridge_responses: Some(&async_response_rx),
abort: Some(&abort_rx),
pause: Some(&pause_control),
},
&pending,
)
};

if matches!(event_loop_status, EventLoopStatus::Terminated) {
terminated = true;
Expand All @@ -2943,6 +2965,13 @@ fn session_thread(
code = next_code;
error = Some(next_error);
}
process_exit_code = process_exit_code
.or_else(|| isolate::process_exit_code(iso));
if process_exit_code.is_some() {
terminated = true;
exports = None;
error = None;
}
}
}
}
Expand Down Expand Up @@ -3063,6 +3092,15 @@ fn session_thread(
code: "ERR_SCRIPT_WALL_CLOCK_EXCEEDED".into(),
}),
}
} else if let Some(process_exit_code) = process_exit_code.filter(|_| {
!matches!(abort_reason, Some(ExecutionAbortReason::Terminated))
}) {
RuntimeEvent::ExecutionResult {
session_id,
exit_code: process_exit_code,
exports: None,
error: None,
}
} else if terminated {
RuntimeEvent::ExecutionResult {
session_id,
Expand Down Expand Up @@ -3254,6 +3292,9 @@ fn run_event_loop_with_readiness(
// passes. Check the durable abort lane before any V8 API call; querying
// promises or timers on an already-terminated isolate can otherwise
// spin forever and prevent explicit session destruction from joining.
if crate::isolate::process_exit_code(scope).is_some() {
return EventLoopStatus::Terminated;
}
if abort_rx.is_some_and(execution_abort_requested) {
scope.terminate_execution();
return EventLoopStatus::Terminated;
Expand All @@ -3270,6 +3311,9 @@ fn run_event_loop_with_readiness(
control.wait_while_paused();
}
pump_v8_message_loop(scope);
if crate::isolate::process_exit_code(scope).is_some() {
return EventLoopStatus::Terminated;
}

// Bound completion work per turn so a response flood cannot starve
// ordinary stream/control events.
Expand Down Expand Up @@ -3302,6 +3346,9 @@ fn run_event_loop_with_readiness(
// a fixed empty-work floor to every readiness and bridge completion.
scope.perform_microtask_checkpoint();
pump_v8_message_loop(scope);
if crate::isolate::process_exit_code(scope).is_some() {
return EventLoopStatus::Terminated;
}

if pending_guest_immediate_count(scope) > 0 {
match try_recv_session_command(scope, rx, ready_rx, ready_broker, bridge_rx, abort_rx) {
Expand Down
23 changes: 23 additions & 0 deletions examples/command-code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
title: "Command Code CLI"
description: "Run the Command Code v1 CLI directly in an agentOS VM."
category: "Agents"
order: 5
---

# Command Code CLI

Run the genuine Command Code v1 CLI inside an agentOS VM. This example invokes `cmd` directly in headless mode; it does not use an ACP adapter.

## Run it

```bash
pnpm install
export COMMAND_CODE_API_KEY=...
pnpm tsx server.ts # starts the registry on http://localhost:6420
pnpm tsx client.ts # runs cmd -p and prints its JSON output
```

## Source

View the source on GitHub: https://github.com/rivet-dev/agentos/tree/main/examples/command-code
32 changes: 32 additions & 0 deletions examples/command-code/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// docs:start quickstart
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({
endpoint: "http://localhost:6420",
});
const agent = client.vm.getOrCreate("my-agent");
const apiKey = process.env.COMMAND_CODE_API_KEY;
if (!apiKey) throw new Error("COMMAND_CODE_API_KEY is required");

await agent.mkdir("/workspace", { recursive: true });
const result = await agent.execArgv(
"cmd",
[
"-p",
"What files are in the current directory?",
"--output-format",
"json",
"--trust",
"--yolo",
"--skip-onboarding",
"--no-auto-update",
],
{
cwd: "/workspace",
env: { COMMAND_CODE_API_KEY: apiKey },
},
);
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout);
console.log(result.stdout);
// docs:end quickstart
Loading
Loading