diff --git a/CLAUDE.md b/CLAUDE.md index f1fb98fe3..101476f88 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,7 @@ Agent OS is the agent-facing wrapper around secure-exec. It provides ACP session - Keep generic runtime, kernel, VFS, language execution, generic registry software, and packaged agent definitions/adapters in secure-exec. - Agent OS owns ACP, sessions, toolkit semantics, quickstarts, docs, and the AgentOs facade. - Call OS instances VMs, never sandboxes. +- Keep root `package.json` scripts limited to Turbo orchestration; put repo-specific commands in `justfile` recipes. - 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 diff --git a/Cargo.lock b/Cargo.lock index d6b8bd466..5a204718c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3036,9 +3036,7 @@ dependencies = [ [[package]] name = "secure-exec-bridge" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca9f44ec9678425b934dd067dab37d7a2ef67ece3ceab5fa53017e01401aad22" +version = "0.3.0-rc.1" dependencies = [ "serde", "serde_json", @@ -3047,9 +3045,7 @@ dependencies = [ [[package]] name = "secure-exec-client" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09341bcd26de78deb5da5a96e34af75809d58c1a8145185ea5016f7be3cf89e" +version = "0.3.0-rc.1" dependencies = [ "futures", "parking_lot", @@ -3062,9 +3058,7 @@ dependencies = [ [[package]] name = "secure-exec-execution" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5258fe9d40867e9558d8be678b92d4c64a45d465194279fe686cc122b646a44c" +version = "0.3.0-rc.1" dependencies = [ "base64 0.22.1", "ciborium", @@ -3080,9 +3074,7 @@ dependencies = [ [[package]] name = "secure-exec-kernel" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39191ccae74b0ea81410492a51f2646e7c3fdc3de591f730d46403c9b00b421a" +version = "0.3.0-rc.1" dependencies = [ "base64 0.22.1", "getrandom 0.2.17", @@ -3096,9 +3088,7 @@ dependencies = [ [[package]] name = "secure-exec-sidecar" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59df6ffad6ace6b51d08e3ebbe6fa31ae19fd5895e39fdb64cdc072d536a8ab4" +version = "0.3.0-rc.1" dependencies = [ "async-trait", "aws-config", @@ -3147,9 +3137,7 @@ dependencies = [ [[package]] name = "secure-exec-v8-runtime" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce8167974ccf57dba1ddd308bcf381061c4f2afe58da7a8a00a7d31a2f2efe84" +version = "0.3.0-rc.1" dependencies = [ "ciborium", "crossbeam-channel", @@ -3163,9 +3151,7 @@ dependencies = [ [[package]] name = "secure-exec-vfs" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1204f74f0b8659e029618808825c26276521609a6cdd877be121a164f8bf6676" +version = "0.3.0-rc.1" dependencies = [ "async-trait", "aws-sdk-s3", @@ -3177,9 +3163,7 @@ dependencies = [ [[package]] name = "secure-exec-vfs-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dbfb44870f07ed7ad842edc95d9c369ade2abcf301a3185d79fd65319a2d83e" +version = "0.3.0-rc.1" dependencies = [ "async-trait", "base64 0.22.1", @@ -3191,9 +3175,7 @@ dependencies = [ [[package]] name = "secure-exec-vm-config" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a7b76b828e2df1d5319b3b8bff98d1fdb0a2c20fba2d2e4280c1ff8f3914f4" +version = "0.3.0-rc.1" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 1de13a07f..208d6bc51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,16 +22,16 @@ repository = "https://github.com/rivet-dev/agent-os" # normal crates.io dependencies so CI/publish builds do not need a sibling # checkout. [workspace.dependencies] -agentos-bridge = { package = "secure-exec-bridge", version = "0.3.2" } +agentos-bridge = { package = "secure-exec-bridge", path = "../secure-exec/crates/bridge", version = "0.3.0-rc.1" } agentos-protocol = { path = "crates/agentos-protocol", version = "0.2.0-rc.3" } agentos-sidecar = { path = "crates/agentos-sidecar", version = "0.2.0-rc.3" } agentos-sidecar-browser = { path = "crates/agentos-sidecar-browser", version = "0.2.0-rc.3" } -agentos-kernel = { package = "secure-exec-kernel", version = "0.3.2" } -agentos-execution = { package = "secure-exec-execution", version = "0.3.2" } -agentos-v8-runtime = { package = "secure-exec-v8-runtime", version = "0.3.2" } -secure-exec-client = { version = "0.3.2" } -secure-exec-bridge = { version = "0.3.2" } -secure-exec-sidecar = { version = "0.3.2" } -secure-exec-vm-config = { version = "0.3.2" } +agentos-kernel = { package = "secure-exec-kernel", path = "../secure-exec/crates/kernel", version = "0.3.0-rc.1" } +agentos-execution = { package = "secure-exec-execution", path = "../secure-exec/crates/execution", version = "0.3.0-rc.1" } +agentos-v8-runtime = { package = "secure-exec-v8-runtime", path = "../secure-exec/crates/v8-runtime", version = "0.3.0-rc.1" } +secure-exec-client = { path = "../secure-exec/crates/secure-exec-client", version = "0.3.0-rc.1" } +secure-exec-bridge = { path = "../secure-exec/crates/bridge", version = "0.3.0-rc.1" } +secure-exec-sidecar = { path = "../secure-exec/crates/sidecar", version = "0.3.0-rc.1" } +secure-exec-vm-config = { path = "../secure-exec/crates/vm-config", version = "0.3.0-rc.1" } vbare = "0.0.4" vbare-compiler = { package = "rivet-vbare-compiler", version = "0.0.5" } diff --git a/TODO.md b/TODO.md index 1d6f8d465..727e7e8ae 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,6 @@ # TODO +- Move root-level npm scripts that are not Turbo orchestration into `justfile` recipes. Keep Turbo commands such as `build`, `start`, `test:watch`, and `check-types` in `package.json`; migrate wrapper/utility scripts such as `test`, `test:migration-parity`, `test:post-python-parity`, `lint`, `fmt`, and `shell`. - Add OCI import/export support for overlay filesystem layers and snapshots after phase 1. The phase-1 API should only guarantee the bundled base filesystem artifact and the internal snapshot export/import format. - Run the full secure-exec registry native-kernel suite after rebuilding the WASM command artifacts. `../secure-exec/registry/tests/smoke.test.ts` skipped in this pass because the local `../secure-exec/registry/native/target/wasm32-wasip1/release/commands` binaries were not present, so the new `createKernel` sidecar-backed path is only verified by package-level builds plus targeted core tests right now. - Expand verification for the new sidecar-backed kernel compatibility surface around `socketTable`/`processTable` observability and browser runtime end-to-end specs. The source builds are green and targeted tests passed, but the deeper integration suites were not exercised in this pass. diff --git a/crates/client/src/shell.rs b/crates/client/src/shell.rs index e7cd09194..af628fe03 100644 --- a/crates/client/src/shell.rs +++ b/crates/client/src/shell.rs @@ -267,6 +267,9 @@ impl AgentOs { .command .clone() .unwrap_or_else(|| DEFAULT_SHELL_COMMAND.to_string()); + options + .env + .insert(String::from("AGENTOS_EXEC_TTY"), String::from("1")); let execute = wire::ExecuteRequest { process_id: process_id.clone(), command: Some(command), diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 1db2f3f79..c37aa6c3b 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -27,11 +27,11 @@ "sandbox-agent": "^0.4.2", "dockerode": "^4.0.9", "get-port": "^7.1.0", - "@agentos-software/git": "catalog:", - "@agentos-software/claude-code": "catalog:", - "@agentos-software/opencode": "catalog:", - "@agentos-software/pi": "catalog:", - "@secure-exec/s3": "catalog:", + "@agentos-software/git": "link:../../../secure-exec/registry/software/git", + "@agentos-software/claude-code": "link:../../../secure-exec/registry/agent/claude", + "@agentos-software/opencode": "link:../../../secure-exec/registry/agent/opencode", + "@agentos-software/pi": "link:../../../secure-exec/registry/agent/pi", + "@secure-exec/s3": "link:../../../secure-exec/registry/file-system/s3", "zod": "^4.1.11" }, "devDependencies": { diff --git a/justfile b/justfile index 0b87a3396..bb0e4ea7f 100644 --- a/justfile +++ b/justfile @@ -33,8 +33,22 @@ secure-exec-set-version VERSION: agentos-pkgs-set-version VERSION: node scripts/secure-exec-dep.mjs set-agentos-pkgs-version "{{ VERSION }}" -dev-shell *args: - pnpm --filter @rivet-dev/agentos-dev-shell dev-shell -- "$@" +install-shell: + #!/usr/bin/env bash + set -euo pipefail + pnpm --filter @rivet-dev/agentos-shell build + global_bin_dir="$(pnpm config get global-bin-dir)" + if [[ -z "$global_bin_dir" || "$global_bin_dir" == "undefined" ]]; then + global_bin_dir="${PNPM_HOME:-/tmp/pnpm}" + fi + mkdir -p "$global_bin_dir" + for package in @rivet-dev/agentos-shell @rivet-dev/agent-os-shell @rivet-dev/agentos-workspace; do + PATH="$global_bin_dir:$PATH" pnpm --global remove "$package" >/dev/null 2>&1 || true + done + (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 "$@" # Run the agentos-sdk.dev site (landing + /docs) locally with hot reload docs: diff --git a/package.json b/package.json index 14ea1f0d2..6f7b054c2 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,9 @@ "scripts": { "start": "npx turbo watch build", "build": "npx turbo build", - "test": "pnpm --dir packages/core build && pnpm --dir packages/dev-shell build && pnpm --dir packages/dev-shell check-types && pnpm --dir packages/dev-shell test && npx turbo test --concurrency=1 --filter='!@rivet-dev/agentos-dev-shell'", + "test": "pnpm --dir packages/core build && npx turbo test --concurrency=1 --filter='!@rivet-dev/agentos-shell'", "test:migration-parity": "pnpm --dir packages/core exec vitest run tests/migration-parity.test.ts --reporter=verbose", - "test:post-python-parity": "pnpm --dir packages/core build && pnpm --dir packages/core exec vitest run tests/agentos-base-filesystem.test.ts && pnpm --dir packages/dev-shell exec vitest run test/dev-shell.integration.test.ts", + "test:post-python-parity": "pnpm --dir packages/core build && pnpm --dir packages/core exec vitest run tests/agentos-base-filesystem.test.ts", "test:watch": "npx turbo watch test", "check-types": "npx turbo check-types --concurrency=1", "lint": "pnpm biome check .", @@ -23,11 +23,11 @@ "@copilotkit/llmock": "^1.6.0", "@mariozechner/pi-coding-agent": "^0.60.0", "@rivet-dev/agentos-core": "workspace:*", - "@agentos-software/claude-code": "catalog:", - "@agentos-software/codex": "catalog:", - "@agentos-software/common": "catalog:", - "@secure-exec/core": "catalog:", - "@agentos-software/pi": "catalog:", + "@agentos-software/claude-code": "link:../secure-exec/registry/agent/claude", + "@agentos-software/codex": "link:../secure-exec/registry/agent/codex", + "@agentos-software/common": "link:../secure-exec/registry/software/common", + "@secure-exec/core": "link:../secure-exec/packages/core", + "@agentos-software/pi": "link:../secure-exec/registry/agent/pi", "@types/node": "^22.19.15", "jszip": "^3.10.1", "pdf-lib": "^1.17.1", diff --git a/packages/agentos-sandbox/package.json b/packages/agentos-sandbox/package.json index 64fb47d3e..d22d0320d 100644 --- a/packages/agentos-sandbox/package.json +++ b/packages/agentos-sandbox/package.json @@ -22,12 +22,12 @@ }, "dependencies": { "@rivet-dev/agentos-core": "workspace:*", - "@secure-exec/sandbox": "catalog:", + "@secure-exec/sandbox": "link:../../../secure-exec/registry/tool/sandbox", "sandbox-agent": "^0.4.2", "zod": "^4.1.11" }, "devDependencies": { - "@agentos-software/common": "catalog:", + "@agentos-software/common": "link:../../../secure-exec/registry/software/common", "@types/node": "^22.10.2", "typescript": "^5.7.2", "vitest": "^2.1.8" diff --git a/packages/agentos/package.json b/packages/agentos/package.json index b0e418a6c..434f7a7ab 100644 --- a/packages/agentos/package.json +++ b/packages/agentos/package.json @@ -51,7 +51,7 @@ "test": "vitest run" }, "dependencies": { - "@agentos-software/common": "catalog:", + "@agentos-software/common": "link:../../../secure-exec/registry/software/common", "@rivet-dev/agentos-core": "workspace:*", "@rivet-dev/agentos-sidecar": "workspace:*", "@rivetkit/react": "0.0.0-feat-dylib-actor-plugin.c44621f", diff --git a/packages/core/package.json b/packages/core/package.json index ea73648b0..b2e7048c4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -54,11 +54,11 @@ "test": "vitest run --reporter=verbose" }, "dependencies": { - "@agentos-software/common": "catalog:", + "@agentos-software/common": "link:../../../secure-exec/registry/software/common", "@aws-sdk/client-s3": "^3.1019.0", "@rivet-dev/agentos-sidecar": "workspace:*", "@rivetkit/bare-ts": "^0.6.2", - "@secure-exec/core": "catalog:", + "@secure-exec/core": "link:../../../secure-exec/packages/core", "@xterm/headless": "^6.0.0", "better-sqlite3": "^12.8.0", "croner": "^10.0.1", @@ -78,31 +78,31 @@ "@bare-ts/tools": "0.15.0", "@copilotkit/llmock": "^1.6.0", "@rivet-dev/agentos-sandbox": "link:../agentos-sandbox", - "@agentos-software/git": "catalog:", - "@secure-exec/google-drive": "catalog:", - "@secure-exec/s3": "catalog:", + "@agentos-software/git": "link:../../../secure-exec/registry/software/git", + "@secure-exec/google-drive": "link:../../../secure-exec/registry/file-system/google-drive", + "@secure-exec/s3": "link:../../../secure-exec/registry/file-system/s3", "@mariozechner/pi-coding-agent": "^0.60.0", - "@agentos-software/claude-code": "catalog:", - "@agentos-software/codex-cli": "catalog:", - "@agentos-software/codex": "catalog:", - "@agentos-software/coreutils": "catalog:", - "@agentos-software/curl": "catalog:", - "@agentos-software/diffutils": "catalog:", - "@agentos-software/fd": "catalog:", - "@agentos-software/file": "catalog:", - "@agentos-software/findutils": "catalog:", - "@agentos-software/gawk": "catalog:", - "@agentos-software/grep": "catalog:", - "@agentos-software/gzip": "catalog:", - "@agentos-software/jq": "catalog:", - "@agentos-software/opencode": "catalog:", - "@agentos-software/pi": "catalog:", - "@agentos-software/pi-cli": "catalog:", - "@agentos-software/ripgrep": "catalog:", - "@agentos-software/sed": "catalog:", - "@agentos-software/tar": "catalog:", - "@agentos-software/tree": "catalog:", - "@agentos-software/yq": "catalog:", + "@agentos-software/claude-code": "link:../../../secure-exec/registry/agent/claude", + "@agentos-software/codex-cli": "link:../../../secure-exec/registry/software/codex", + "@agentos-software/codex": "link:../../../secure-exec/registry/agent/codex", + "@agentos-software/coreutils": "link:../../../secure-exec/registry/software/coreutils", + "@agentos-software/curl": "link:../../../secure-exec/registry/software/curl", + "@agentos-software/diffutils": "link:../../../secure-exec/registry/software/diffutils", + "@agentos-software/fd": "link:../../../secure-exec/registry/software/fd", + "@agentos-software/file": "link:../../../secure-exec/registry/software/file", + "@agentos-software/findutils": "link:../../../secure-exec/registry/software/findutils", + "@agentos-software/gawk": "link:../../../secure-exec/registry/software/gawk", + "@agentos-software/grep": "link:../../../secure-exec/registry/software/grep", + "@agentos-software/gzip": "link:../../../secure-exec/registry/software/gzip", + "@agentos-software/jq": "link:../../../secure-exec/registry/software/jq", + "@agentos-software/opencode": "link:../../../secure-exec/registry/agent/opencode", + "@agentos-software/pi": "link:../../../secure-exec/registry/agent/pi", + "@agentos-software/pi-cli": "link:../../../secure-exec/registry/agent/pi-cli", + "@agentos-software/ripgrep": "link:../../../secure-exec/registry/software/ripgrep", + "@agentos-software/sed": "link:../../../secure-exec/registry/software/sed", + "@agentos-software/tar": "link:../../../secure-exec/registry/software/tar", + "@agentos-software/tree": "link:../../../secure-exec/registry/software/tree", + "@agentos-software/yq": "link:../../../secure-exec/registry/software/yq", "@types/node": "^22.10.2", "pi-acp": "^0.0.23", "sandbox-agent": "^0.4.2", diff --git a/packages/core/src/agent-os.ts b/packages/core/src/agent-os.ts index d9addda9a..70e160248 100644 --- a/packages/core/src/agent-os.ts +++ b/packages/core/src/agent-os.ts @@ -347,7 +347,7 @@ interface AcpTerminalEntry { interface ShellEntry { handle: ShellHandle; dataHandlers: Set<(data: Uint8Array) => void>; - exitPromise: Promise; + exitPromise: Promise; } export type RootLowerInput = @@ -2698,7 +2698,7 @@ export class AgentOs { private _closedShellIds = new BoundedSet( CLOSED_SHELL_ID_RETENTION_LIMIT, ); - private _pendingShellExitPromises = new Set>(); + private _pendingShellExitPromises = new Set>(); private _shellCounter = 0; private _acpTerminals = new Map(); private _acpTerminalCounter = 0; @@ -3142,17 +3142,17 @@ export class AgentOs { } /** Write data to a process's stdin. */ - writeProcessStdin(pid: number, data: string | Uint8Array): void { + writeProcessStdin(pid: number, data: string | Uint8Array): Promise { const entry = this._processes.get(pid); if (!entry) throw new Error(`Process not found: ${pid}`); - entry.proc.writeStdin(data); + return entry.proc.writeStdin(data); } /** Close a process's stdin stream. */ - closeProcessStdin(pid: number): void { + closeProcessStdin(pid: number): Promise { const entry = this._processes.get(pid); if (!entry) throw new Error(`Process not found: ${pid}`); - entry.proc.closeStdin(); + return entry.proc.closeStdin(); } /** Subscribe to stdout data from a process. Returns an unsubscribe function. */ @@ -3487,12 +3487,9 @@ export class AgentOs { const entry: ShellEntry = { handle, dataHandlers, - exitPromise: Promise.resolve(), + exitPromise: Promise.resolve(0), }; - const exitPromise = handle.wait().then( - () => undefined, - () => undefined, - ); + const exitPromise = handle.wait(); entry.exitPromise = exitPromise.finally(() => { this._pendingShellExitPromises.delete(entry.exitPromise); if (this._shells.get(shellId) === entry) { @@ -3510,10 +3507,10 @@ export class AgentOs { } /** Write data to a shell's PTY input. */ - writeShell(shellId: string, data: string | Uint8Array): void { + writeShell(shellId: string, data: string | Uint8Array): Promise { const entry = this._shells.get(shellId); if (!entry) throw new Error(`Shell not found: ${shellId}`); - entry.handle.write(data); + return entry.handle.write(data); } /** Subscribe to data output from a shell. Returns an unsubscribe function. */ @@ -3536,6 +3533,13 @@ export class AgentOs { entry.handle.resize(cols, rows); } + /** Wait for a shell to exit and return its process exit code. */ + waitShell(shellId: string): Promise { + const entry = this._shells.get(shellId); + if (!entry) throw new Error(`Shell not found: ${shellId}`); + return entry.exitPromise; + } + /** Kill a shell process and remove it from tracking. */ closeShell(shellId: string): void { const entry = this._shells.get(shellId); diff --git a/packages/core/src/runtime-compat.ts b/packages/core/src/runtime-compat.ts index 3332fdc6d..e2bce3578 100644 --- a/packages/core/src/runtime-compat.ts +++ b/packages/core/src/runtime-compat.ts @@ -202,8 +202,8 @@ export interface ProcessInfo { export interface ManagedProcess { pid: number; - writeStdin(data: Uint8Array | string): void; - closeStdin(): void; + writeStdin(data: Uint8Array | string): Promise; + closeStdin(): Promise; kill(signal?: number): void; wait(): Promise; readonly exitCode: number | null; @@ -211,7 +211,7 @@ export interface ManagedProcess { export interface ShellHandle { pid: number; - write(data: Uint8Array | string): void; + write(data: Uint8Array | string): Promise; onData: ((data: Uint8Array) => void) | null; resize(cols: number, rows: number): void; kill(signal?: number): void; @@ -2672,10 +2672,10 @@ class NativeKernel implements Kernel { return { pid: proc.pid, writeStdin(data) { - proc.writeStdin(data); + return proc.writeStdin(data); }, closeStdin() { - proc.closeStdin(); + return proc.closeStdin(); }, kill(signal) { proc.kill(signal); diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index c0abe33eb..37512c452 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -111,8 +111,8 @@ export interface ProcessInfo { export interface ManagedProcess { pid: number; - writeStdin(data: Uint8Array | string): void; - closeStdin(): void; + writeStdin(data: Uint8Array | string): Promise; + closeStdin(): Promise; kill(signal?: number): void; wait(): Promise; readonly exitCode: number | null; @@ -120,7 +120,7 @@ export interface ManagedProcess { export interface ShellHandle { pid: number; - write(data: Uint8Array | string): void; + write(data: Uint8Array | string): Promise; onData: ((data: Uint8Array) => void) | null; resize(cols: number, rows: number): void; kill(signal?: number): void; diff --git a/packages/core/src/sidecar/rpc-client.ts b/packages/core/src/sidecar/rpc-client.ts index 969704d04..e9833f2b3 100644 --- a/packages/core/src/sidecar/rpc-client.ts +++ b/packages/core/src/sidecar/rpc-client.ts @@ -46,6 +46,42 @@ const TRAILING_OUTPUT_DRAIN_INTERVAL_MS = 10; const TRAILING_OUTPUT_DRAIN_MAX_MS = 250; const TRAILING_OUTPUT_DRAIN_QUIET_TURNS = 2; +function shouldLogStructuredSidecarEvent(name: string): boolean { + const normalized = name.toLowerCase(); + return ( + normalized === "limit_warning" || + normalized.startsWith("security.") || + normalized.includes("warning") || + normalized.includes("failed") || + normalized.includes("error") + ); +} + +function formatStructuredSidecarDetail( + detail: Readonly>, +): string { + const entries = Object.entries(detail); + if (entries.length === 0) { + return ""; + } + return entries.map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(" "); +} + +function logStructuredSidecarEvent( + name: string, + detail: Readonly>, +): void { + if (!shouldLogStructuredSidecarEvent(name)) { + return; + } + const formatted = formatStructuredSidecarDetail(detail); + console.warn( + formatted + ? `[agent-os] sidecar ${name}: ${formatted}` + : `[agent-os] sidecar ${name}`, + ); +} + async function drainTrailingProcessOutputTurn(delayMs = 0): Promise { // Native-sidecar `process_output` events can lag one macrotask behind the // terminal `process_exited` notification for very short-lived processes, and @@ -774,16 +810,16 @@ export class NativeSidecarKernelProxy { pid, writeStdin: (data) => { if (entry.exitCode !== null) { - return; + return Promise.resolve(); } entry.pendingStdin.push(data); - void this.flushPendingStdin(entry).catch((error) => { + return this.flushPendingStdin(entry).catch((error) => { this.handleBackgroundProcessError(entry, error); }); }, closeStdin: () => { entry.pendingCloseStdin = true; - void this.closeTrackedStdin(entry).catch((error) => { + return this.closeTrackedStdin(entry).catch((error) => { this.handleBackgroundProcessError(entry, error); }); }, @@ -832,8 +868,6 @@ export class NativeSidecarKernelProxy { options?.args ?? (command === "sh" || command === "/bin/sh" ? ["-i"] : []); const synthesizePrompt = !options?.command && !options?.args; - const autoCloseExplicitCommandStdin = - Boolean(options?.command) && !["sh", "/bin/sh", "bash"].includes(command); const promptText = "sh-0.4$ "; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); @@ -844,14 +878,21 @@ export class NativeSidecarKernelProxy { .replace(/\u001b\[[0-9;]*m/g, "") .replace(/^.*WARN could not retrieve pid for child process\n?/gm, "") .replace(/^ProcessExitError:.*\n(?:\s+at .*\n)*/gm, ""); + const sanitizeNativeShellOutput = (chunk: Uint8Array) => { + const text = textDecoder.decode(chunk); + const sanitized = text.replace( + /^.*WARN could not retrieve pid for child process\n?/gm, + "", + ); + return sanitized.length > 0 ? textEncoder.encode(sanitized) : null; + }; let bufferedInput = ""; let bufferedCommand = ""; let activeForegroundProcess: ManagedProcess | null = null; - let shellEnv = { ...(options?.env ?? {}) }; + let shellEnv = { ...(options?.env ?? {}), AGENTOS_EXEC_TTY: "1" }; let shellCwd = options?.cwd ?? this.cwd; let syntheticCommandQueue = Promise.resolve(); let promptTimer: ReturnType | null = null; - let closeStdinTimer: ReturnType | null = null; let commandInFlight = false; let syntheticCursorAtLineStart = true; const syntheticPid = this.nextSyntheticPid++; @@ -866,12 +907,6 @@ export class NativeSidecarKernelProxy { promptTimer = null; } }; - const clearCloseStdinTimer = () => { - if (closeStdinTimer !== null) { - clearTimeout(closeStdinTimer); - closeStdinTimer = null; - } - }; const normalizeSyntheticTerminalText = (text: string) => text.replace(/\r?\n/g, "\r\n"); const updateSyntheticCursor = (text: string) => { @@ -970,20 +1005,40 @@ export class NativeSidecarKernelProxy { } return parsed; }; - const writeForegroundInput = ( - proc: ManagedProcess, - data: string | Uint8Array, - ) => { + const writeForegroundInput = async ( + proc: ManagedProcess, + data: string | Uint8Array, + ) => { if (typeof data === "string") { for (const character of data) { - proc.writeStdin(character); + await proc.writeStdin(character); } return; } - for (const byte of data) { - proc.writeStdin(new Uint8Array([byte])); - } - }; + for (const byte of data) { + await proc.writeStdin(new Uint8Array([byte])); + } + }; + const appendSyntheticInput = (input: string) => { + for (const character of input) { + if (character === "\u0004") { + continue; + } + if (character === "\b" || character === "\u007f") { + if (bufferedInput.length > 0) { + bufferedInput = bufferedInput.slice(0, -1); + emitSyntheticTerminal("\b \b"); + } + continue; + } + bufferedInput += character; + if (character === "\n") { + emitSyntheticTerminal("\n"); + } else if (character >= " ") { + emitSyntheticTerminal(character); + } + } + }; let onData: ((data: Uint8Array) => void) | null = null; stdoutHandlers.add((data) => onData?.(data)); @@ -994,27 +1049,30 @@ export class NativeSidecarKernelProxy { schedulePrompt(0); return { pid: syntheticPid, - write(data) { - if (syntheticExitCode !== null) { - return; - } + async write(data) { + if (syntheticExitCode !== null) { + return; + } if (activeForegroundProcess) { const rawText = typeof data === "string" ? data : Buffer.from(data).toString("utf8"); - if (rawText.includes("\u0003")) { - const [beforeInterrupt] = rawText.split("\u0003"); - if (beforeInterrupt) { - writeForegroundInput(activeForegroundProcess, beforeInterrupt); + if (rawText.includes("\u0003")) { + const [beforeInterrupt] = rawText.split("\u0003"); + if (beforeInterrupt) { + await writeForegroundInput( + activeForegroundProcess, + beforeInterrupt, + ); + } + emitSyntheticTerminal("^C\n"); + activeForegroundProcess.kill(2); + return; } - emitSyntheticTerminal("^C\n"); - activeForegroundProcess.kill(2); + await writeForegroundInput(activeForegroundProcess, data); return; } - writeForegroundInput(activeForegroundProcess, data); - return; - } const rawText = typeof data === "string" ? data @@ -1038,19 +1096,20 @@ export class NativeSidecarKernelProxy { finishSyntheticShell(0); return; } - bufferedInput += text.replace(/\u0004/g, ""); - while (true) { - const newlineIndex = bufferedInput.indexOf("\n"); - if (newlineIndex < 0) { - break; + appendSyntheticInput( + text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"), + ); + while (true) { + const newlineIndex = bufferedInput.indexOf("\n"); + if (newlineIndex < 0) { + break; } - const line = bufferedInput - .slice(0, newlineIndex) - .replace(/\r$/, ""); - bufferedInput = bufferedInput.slice(newlineIndex + 1); - emitSyntheticStdout(`${line}\n`); - const nextCommand = bufferedCommand - ? `${bufferedCommand}\n${line}` + const line = bufferedInput + .slice(0, newlineIndex) + .replace(/\r$/, ""); + bufferedInput = bufferedInput.slice(newlineIndex + 1); + const nextCommand = bufferedCommand + ? `${bufferedCommand}\n${line}` : line; if (commandNeedsContinuation(nextCommand)) { bufferedCommand = nextCommand; @@ -1165,20 +1224,33 @@ export class NativeSidecarKernelProxy { } const proc = this.spawn(command, args, { - env: options?.env, + env: { + ...(options?.env ?? {}), + ...(options?.cols ? { COLUMNS: String(Math.trunc(options.cols)) } : {}), + ...(options?.rows ? { LINES: String(Math.trunc(options.rows)) } : {}), + AGENTOS_EXEC_TTY: "1", + }, cwd: options?.cwd, streamStdin: true, onStdout: (chunk) => { + const sanitized = sanitizeNativeShellOutput(chunk); + if (!sanitized) { + return; + } for (const handler of stdoutHandlers) { - handler(chunk); + handler(sanitized); } if (commandInFlight) { schedulePrompt(120); } }, onStderr: (chunk) => { + const sanitized = sanitizeNativeShellOutput(chunk); + if (!sanitized) { + return; + } for (const handler of stderrHandlers) { - handler(chunk); + handler(sanitized); } if (commandInFlight) { schedulePrompt(120); @@ -1188,18 +1260,11 @@ export class NativeSidecarKernelProxy { return { pid: proc.pid, - write(data) { + async write(data) { if (synthesizePrompt) { return; } - proc.writeStdin(data); - if (autoCloseExplicitCommandStdin) { - clearCloseStdinTimer(); - closeStdinTimer = setTimeout(() => { - closeStdinTimer = null; - proc.closeStdin(); - }, 100); - } + await proc.writeStdin(data); if ( synthesizePrompt && typeof data === "string" && @@ -1215,11 +1280,26 @@ export class NativeSidecarKernelProxy { set onData(handler) { onData = handler; }, - resize() { - // The current stdio-native path is process-backed rather than PTY-backed. + resize: (cols, rows) => { + const entry = this.trackedProcesses.get(proc.pid); + if (!entry || entry.exitCode !== null) { + return; + } + void entry.startPromise + .then(() => + this.client.resizePty( + this.session, + this.vm, + entry.processId, + Math.trunc(cols), + Math.trunc(rows), + ), + ) + .catch((error) => { + this.handleBackgroundProcessError(entry, error); + }); }, kill(signal) { - clearCloseStdinTimer(); clearPromptTimer(); proc.kill(signal); }, @@ -1674,6 +1754,11 @@ export class NativeSidecarKernelProxy { } void this.refreshProcessSnapshot().catch(() => {}); this.finishProcess(entry, event.payload.exit_code); + continue; + } + + if (event.payload.type === "structured") { + logStructuredSidecarEvent(event.payload.name, event.payload.detail); } } catch (error) { if (this.disposed) { diff --git a/packages/core/tests/__snapshots__/brush-interactive.test.ts.snap b/packages/core/tests/__snapshots__/brush-interactive.test.ts.snap new file mode 100644 index 000000000..ba7b4f005 --- /dev/null +++ b/packages/core/tests/__snapshots__/brush-interactive.test.ts.snap @@ -0,0 +1,77 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`brush interactive PTY repaint > Enter preserves scrollback; history and word-edit work 1`] = ` +"# startup prompt +cursor=5,0 +01|AOS$ +02| +03| +04| +05| +06| +07| +08| +09| +10| +11| +12| +13| +14|" +`; + +exports[`brush interactive PTY repaint > Enter preserves scrollback; history and word-edit work 2`] = ` +"# after three commands (scrollback intact) +cursor=5,9 +01|AOS$ echo alpha +02|alpha +03| +04|AOS$ echo bravo +05|bravo +06| +07|AOS$ echo charlie +08|charlie +09| +10|AOS$ +11| +12| +13| +14|" +`; + +exports[`brush interactive PTY repaint > Enter preserves scrollback; history and word-edit work 3`] = ` +"# after up-arrow recall +cursor=17,9 +01|AOS$ echo alpha +02|alpha +03| +04|AOS$ echo bravo +05|bravo +06| +07|AOS$ echo charlie +08|charlie +09| +10|AOS$ echo charlie +11| +12| +13| +14|" +`; + +exports[`brush interactive PTY repaint > Enter preserves scrollback; history and word-edit work 4`] = ` +"# after ctrl-w edit + enter +cursor=5,12 +01|AOS$ echo alpha +02|alpha +03| +04|AOS$ echo bravo +05|bravo +06| +07|AOS$ echo charlie +08|charlie +09| +10|AOS$ echo delta +11|delta +12| +13|AOS$ +14|" +`; diff --git a/packages/core/tests/__snapshots__/pty-protocol.test.ts.snap b/packages/core/tests/__snapshots__/pty-protocol.test.ts.snap new file mode 100644 index 000000000..d117e2af7 --- /dev/null +++ b/packages/core/tests/__snapshots__/pty-protocol.test.ts.snap @@ -0,0 +1,120 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`PTY protocol snapshots > C WASM probe snapshots raw, cooked, CPR, resize, and EOF terminal protocol 1`] = ` +"# startup through CPR +cols=80 rows=18 cursor=11,8 +01|PTY_PROBE start +02|TTY_HOST stdin=1 stdout=1 stderr=1 +03|TTY_LIBC stdin=0 stdout=0 stderr=0 +04|SIZE_START env_cols=80 env_rows=18 host_rc=0 host_cols=80 host_rows=18 ioctl_rc= +05|-1 ioctl_errno=28 ioctl_cols=0 ioctl_rows=0 +06|RAW_ON rc=0 +07|CPR_REQUEST +08|CPR_REPLY bytes=7 hex=1B 5B 37 3B 31 33 52 text=\\e[7;13R +09|RAW_INPUT> +10| +11| +12| +13| +14| +15| +16| +17| +18|" +`; + +exports[`PTY protocol snapshots > C WASM probe snapshots raw, cooked, CPR, resize, and EOF terminal protocol 2`] = ` +"# after raw input bytes +cols=80 rows=18 cursor=14,11 +01|PTY_PROBE start +02|TTY_HOST stdin=1 stdout=1 stderr=1 +03|TTY_LIBC stdin=0 stdout=0 stderr=0 +04|SIZE_START env_cols=80 env_rows=18 host_rc=0 host_cols=80 host_rows=18 ioctl_rc= +05|-1 ioctl_errno=28 ioctl_cols=0 ioctl_rows=0 +06|RAW_ON rc=0 +07|CPR_REQUEST +08|CPR_REPLY bytes=7 hex=1B 5B 37 3B 31 33 52 text=\\e[7;13R +09|RAW_INPUT> +10|RAW_BYTES bytes=7 hex=41 0A 1B 5B 41 17 21 text=A\\n\\e[A\\x17! +11|RAW_OFF rc=0 +12|COOKED_INPUT> +13| +14| +15| +16| +17| +18|" +`; + +exports[`PTY protocol snapshots > C WASM probe snapshots raw, cooked, CPR, resize, and EOF terminal protocol 3`] = ` +"# after cooked enter +cols=80 rows=18 cursor=14,13 +01|PTY_PROBE start +02|TTY_HOST stdin=1 stdout=1 stderr=1 +03|TTY_LIBC stdin=0 stdout=0 stderr=0 +04|SIZE_START env_cols=80 env_rows=18 host_rc=0 host_cols=80 host_rows=18 ioctl_rc= +05|-1 ioctl_errno=28 ioctl_cols=0 ioctl_rows=0 +06|RAW_ON rc=0 +07|CPR_REQUEST +08|CPR_REPLY bytes=7 hex=1B 5B 37 3B 31 33 52 text=\\e[7;13R +09|RAW_INPUT> +10|RAW_BYTES bytes=7 hex=41 0A 1B 5B 41 17 21 text=A\\n\\e[A\\x17! +11|RAW_OFF rc=0 +12|COOKED_INPUT> COOKED_BYTES bytes=13 hex=68 65 6C 6C 6F 20 63 6F 6F 6B 65 64 0A t +13|ext=hello cooked\\n +14|RESIZE_READY> +15| +16| +17| +18|" +`; + +exports[`PTY protocol snapshots > C WASM probe snapshots raw, cooked, CPR, resize, and EOF terminal protocol 4`] = ` +"# after resize trigger +cols=100 rows=20 cursor=11,15 +01|PTY_PROBE start +02|TTY_HOST stdin=1 stdout=1 stderr=1 +03|TTY_LIBC stdin=0 stdout=0 stderr=0 +04|SIZE_START env_cols=80 env_rows=18 host_rc=0 host_cols=80 host_rows=18 ioctl_rc=-1 ioctl_errno=28 io +05|ctl_cols=0 ioctl_rows=0 +06|RAW_ON rc=0 +07|CPR_REQUEST +08|CPR_REPLY bytes=7 hex=1B 5B 37 3B 31 33 52 text=\\e[7;13R +09|RAW_INPUT> +10|RAW_BYTES bytes=7 hex=41 0A 1B 5B 41 17 21 text=A\\n\\e[A\\x17! +11|RAW_OFF rc=0 +12|COOKED_INPUT> COOKED_BYTES bytes=13 hex=68 65 6C 6C 6F 20 63 6F 6F 6B 65 64 0A text=hello cooked\\n +13|RESIZE_READY> RESIZE_TRIGGER bytes=11 hex=72 65 73 69 7A 65 2D 6E 6F 77 0A text=resize-now\\n +14|SIZE_AFTER_RESIZE env_cols=80 env_rows=18 host_rc=0 host_cols=100 host_rows=20 ioctl_rc=-1 ioctl_err +15|no=28 ioctl_cols=0 ioctl_rows=0 +16|EOF_READY> +17| +18| +19| +20|" +`; + +exports[`PTY protocol snapshots > C WASM probe snapshots raw, cooked, CPR, resize, and EOF terminal protocol 5`] = ` +"# after eof +cols=100 rows=20 cursor=0,17 +01|PTY_PROBE start +02|TTY_HOST stdin=1 stdout=1 stderr=1 +03|TTY_LIBC stdin=0 stdout=0 stderr=0 +04|SIZE_START env_cols=80 env_rows=18 host_rc=0 host_cols=80 host_rows=18 ioctl_rc=-1 ioctl_errno=28 io +05|ctl_cols=0 ioctl_rows=0 +06|RAW_ON rc=0 +07|CPR_REQUEST +08|CPR_REPLY bytes=7 hex=1B 5B 37 3B 31 33 52 text=\\e[7;13R +09|RAW_INPUT> +10|RAW_BYTES bytes=7 hex=41 0A 1B 5B 41 17 21 text=A\\n\\e[A\\x17! +11|RAW_OFF rc=0 +12|COOKED_INPUT> COOKED_BYTES bytes=13 hex=68 65 6C 6C 6F 20 63 6F 6F 6B 65 64 0A text=hello cooked\\n +13|RESIZE_READY> RESIZE_TRIGGER bytes=11 hex=72 65 73 69 7A 65 2D 6E 6F 77 0A text=resize-now\\n +14|SIZE_AFTER_RESIZE env_cols=80 env_rows=18 host_rc=0 host_cols=100 host_rows=20 ioctl_rc=-1 ioctl_err +15|no=28 ioctl_cols=0 ioctl_rows=0 +16|EOF_READY> EOF_READ n=0 +17|PTY_PROBE done +18| +19| +20|" +`; diff --git a/packages/core/tests/brush-interactive.test.ts b/packages/core/tests/brush-interactive.test.ts new file mode 100644 index 000000000..de6b896b6 --- /dev/null +++ b/packages/core/tests/brush-interactive.test.ts @@ -0,0 +1,144 @@ +// Interactive-shell PTY regression test. +// +// brush's interactive line editor (reedline → crossterm) renders through the +// guest PTY. Reedline anchors its prompt at the row reported by +// `crossterm::cursor::position()`. The WASI crossterm port used to stub that to +// `(0, 0)`, so every repaint did `MoveTo(0,0)` + `Clear(FromCursorDown)` and +// wiped the whole screen — most visibly, pressing Enter erased the command and +// its output. The fix makes WASI `position()` issue a real DSR (`ESC[6n`) query +// and read the CPR reply, exactly like the Unix backend. +// +// This test drives the real brush shell over the sidecar shell API, renders the +// output with a headless xterm terminal emulator (which answers DSR like a real +// terminal), and snapshots the screen after each interactive step so a human can +// eyeball that scrollback survives, history recall works, and word-edit works. +// +// The guest command is staged under a UNIQUE name: a command literally named +// `sh` collides with the base-filesystem `/bin/sh` and the VM would run that +// default shell instead of the registry build under test. + +import { copyFileSync, existsSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Terminal } from "@xterm/headless"; +import { afterEach, beforeAll, describe, expect, test } from "vitest"; +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 REGISTRY_SH = resolve( + REPO_ROOT, + "../secure-exec/registry/native/target/wasm32-wasip1/release/commands/sh", +); +const FIXTURE_COMMAND = "brushsh"; // unique name so it does not shadow /bin/sh + +let fixtureDir: string; + +function snapshot(label: string, term: Terminal): string { + const buffer = term.buffer.active; + const lines: string[] = []; + for (let row = 0; row < term.rows; row++) { + const line = buffer.getLine(buffer.viewportY + row); + lines.push( + `${String(row + 1).padStart(2, "0")}|${line ? line.translateToString(true).replace(/\s+$/, "") : ""}`, + ); + } + return [`# ${label}`, `cursor=${buffer.cursorX},${buffer.cursorY}`, ...lines].join("\n"); +} + +async function waitFor(term: Terminal, text: string, timeoutMs = 20000): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (snapshot("w", term).includes(text)) { + await new Promise((r) => setTimeout(r, 150)); + return; + } + await new Promise((r) => setTimeout(r, 25)); + } + throw new Error(`timeout waiting for ${JSON.stringify(text)}\n${snapshot("timeout", term)}`); +} + +describe("brush interactive PTY repaint", () => { + let vm: AgentOs | undefined; + let term: Terminal | undefined; + let shellId: string | undefined; + + beforeAll(() => { + if (!existsSync(REGISTRY_SH)) { + throw new Error( + `registry sh wasm not built at ${REGISTRY_SH}; run 'make' in ../secure-exec/registry/native`, + ); + } + fixtureDir = mkdtempSync(join(tmpdir(), "brush-fixture-")); + copyFileSync(REGISTRY_SH, join(fixtureDir, FIXTURE_COMMAND)); + process.env.AGENTOS_SIDECAR_BIN = SIDECAR_BINARY; + }); + + afterEach(async () => { + if (vm && shellId) { + try { + vm.closeShell(shellId); + } catch { + // already exited + } + } + term?.dispose(); + if (vm) await vm.dispose(); + vm = term = shellId = undefined; + }); + + test("Enter preserves scrollback; history and word-edit work", async () => { + const { AgentOs } = await import("../src/index.js"); + term = new Terminal({ cols: 80, rows: 14, allowProposedApi: true }); + vm = await AgentOs.create({ + software: [ + { + name: "brush-fixture", + type: "wasm-commands", + commandDir: fixtureDir, + commands: [{ name: FIXTURE_COMMAND }], + }, + ], + }); + + ({ shellId } = vm.openShell({ + command: FIXTURE_COMMAND, + args: ["--input-backend", "reedline", "-i"], + cols: term.cols, + rows: term.rows, + env: { TERM: "xterm-256color", PS1: "AOS$ ", COLUMNS: "80", LINES: "14" }, + // A real PTY merges stdout+stderr; brush paints its prompt on stderr. + onStderr: (d: Uint8Array) => term?.write(d), + })); + vm.onShellData(shellId, (d) => term?.write(d)); + const t = term; + const s = shellId; + const v = vm; + // Forwarding xterm's responses back makes it answer DSR (`ESC[6n`) queries. + t.onData((d) => v.writeShell(s, d)); + + await waitFor(t, "AOS$"); + expect(snapshot("startup prompt", t)).toMatchSnapshot(); + + // Run three commands. Each output must remain on screen after Enter. + for (const word of ["alpha", "bravo", "charlie"]) { + v.writeShell(s, `echo ${word}\r`); + await waitFor(t, word); + } + expect(snapshot("after three commands (scrollback intact)", t)).toMatchSnapshot(); + + // Up-arrow recalls the last command ("echo charlie"). + v.writeShell(s, "\x1b[A"); + await new Promise((r) => setTimeout(r, 300)); + expect(snapshot("after up-arrow recall", t)).toMatchSnapshot(); + + // Ctrl-W deletes the recalled word ("charlie"), then type a new one and run it. + v.writeShell(s, "\x17delta\r"); + // Wait for the new command's output line, then settle. + await waitFor(t, "echo delta"); + await new Promise((r) => setTimeout(r, 400)); + expect(snapshot("after ctrl-w edit + enter", t)).toMatchSnapshot(); + }, 60000); +}); diff --git a/packages/core/tests/pty-protocol.test.ts b/packages/core/tests/pty-protocol.test.ts new file mode 100644 index 000000000..818dc3882 --- /dev/null +++ b/packages/core/tests/pty-protocol.test.ts @@ -0,0 +1,205 @@ +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Terminal } from "@xterm/headless"; +import { afterEach, describe, expect, test } from "vitest"; +import type { AgentOs } from "../src/index.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = resolve(__dirname, "../../.."); +const SECURE_EXEC_C_ROOT = resolve( + REPO_ROOT, + "../secure-exec/registry/native/c", +); +const SIDECAR_BINARY = resolve(REPO_ROOT, "target/debug/agentos-sidecar"); +const PTY_PROBE_COMMAND_DIR = resolve(SECURE_EXEC_C_ROOT, "build"); +const PTY_PROBE_BINARY = resolve(PTY_PROBE_COMMAND_DIR, "pty_probe"); + +const SETTLE_MS = 80; +const WAIT_TIMEOUT_MS = 15_000; + +function ensurePtyProbeBuilt(): void { + if (existsSync(PTY_PROBE_BINARY)) { + return; + } + + const result = spawnSync("make", ["build/pty_probe"], { + cwd: SECURE_EXEC_C_ROOT, + encoding: "utf8", + }); + if (result.status !== 0) { + throw new Error( + [ + "failed to build pty_probe C WASM fixture", + `cwd=${SECURE_EXEC_C_ROOT}`, + `status=${result.status}`, + result.stdout, + result.stderr, + ] + .filter(Boolean) + .join("\n"), + ); + } +} + +function ensureWorkspaceSidecarBuilt(): void { + if (!existsSync(SIDECAR_BINARY)) { + const result = spawnSync("cargo", ["build", "-q", "-p", "agentos-sidecar"], { + cwd: REPO_ROOT, + encoding: "utf8", + }); + if (result.status !== 0) { + throw new Error( + [ + "failed to build workspace agentos-sidecar", + `cwd=${REPO_ROOT}`, + `status=${result.status}`, + result.stdout, + result.stderr, + ] + .filter(Boolean) + .join("\n"), + ); + } + } + process.env.AGENTOS_SIDECAR_BIN = SIDECAR_BINARY; +} + +function terminalSnapshot(label: string, term: Terminal): string { + const buffer = term.buffer.active; + const lines: string[] = []; + for (let row = 0; row < term.rows; row++) { + const line = buffer.getLine(buffer.viewportY + row); + lines.push( + `${String(row + 1).padStart(2, "0")}|${ + line ? line.translateToString(true) : "" + }`, + ); + } + + return [ + `# ${label}`, + `cols=${term.cols} rows=${term.rows} cursor=${buffer.cursorX},${buffer.cursorY}`, + ...lines, + ].join("\n"); +} + +async function settle(): Promise { + await new Promise((resolve) => setTimeout(resolve, SETTLE_MS)); +} + +async function waitForScreen( + term: Terminal, + text: string, + timeoutMs = WAIT_TIMEOUT_MS, +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (terminalSnapshot("wait", term).includes(text)) { + await settle(); + return; + } + await new Promise((resolve) => setTimeout(resolve, 25)); + } + throw new Error( + `timed out waiting for ${JSON.stringify(text)}\n${terminalSnapshot( + "timeout", + term, + )}`, + ); +} + +describe("PTY protocol snapshots", () => { + let vm: AgentOs | undefined; + let term: Terminal | undefined; + let shellId: string | undefined; + let unsubscribeShellData: (() => void) | undefined; + let disposeTerminalData: { dispose(): void } | undefined; + + afterEach(async () => { + if (unsubscribeShellData) { + unsubscribeShellData(); + unsubscribeShellData = undefined; + } + if (disposeTerminalData) { + disposeTerminalData.dispose(); + disposeTerminalData = undefined; + } + if (vm && shellId) { + try { + vm.closeShell(shellId); + } catch { + // The probe may already have exited. + } + } + term?.dispose(); + term = undefined; + shellId = undefined; + if (vm) { + await vm.dispose(); + vm = undefined; + } + }); + + test("C WASM probe snapshots raw, cooked, CPR, resize, and EOF terminal protocol", async () => { + ensureWorkspaceSidecarBuilt(); + ensurePtyProbeBuilt(); + const { AgentOs } = await import("../src/index.js"); + + term = new Terminal({ cols: 80, rows: 18, allowProposedApi: true }); + vm = await AgentOs.create({ + software: [ + { + name: "pty-probe-fixture", + type: "wasm-commands", + commandDir: PTY_PROBE_COMMAND_DIR, + commands: [{ name: "pty_probe" }], + }, + ], + }); + + ({ shellId } = vm.openShell({ + command: "pty_probe", + cols: term.cols, + rows: term.rows, + env: { + TERM: "xterm-256color", + COLUMNS: String(term.cols), + LINES: String(term.rows), + }, + })); + + unsubscribeShellData = vm.onShellData(shellId, (data) => { + term?.write(data); + }); + disposeTerminalData = term.onData((data) => { + if (vm && shellId) { + vm.writeShell(shellId, data); + } + }); + + await waitForScreen(term, "RAW_INPUT>"); + expect(terminalSnapshot("startup through CPR", term)).toMatchSnapshot(); + + vm.writeShell(shellId, "A\r\x1b[A\x17!"); + await waitForScreen(term, "COOKED_INPUT>"); + expect(terminalSnapshot("after raw input bytes", term)).toMatchSnapshot(); + + vm.writeShell(shellId, "hello cooked\r"); + await waitForScreen(term, "RESIZE_READY>"); + expect(terminalSnapshot("after cooked enter", term)).toMatchSnapshot(); + + term.resize(100, 20); + vm.resizeShell(shellId, 100, 20); + vm.writeShell(shellId, "resize-now\r"); + await waitForScreen(term, "EOF_READY>"); + expect(terminalSnapshot("after resize trigger", term)).toMatchSnapshot(); + + vm.writeShell(shellId, "\x04"); + await waitForScreen(term, "PTY_PROBE done"); + expect(terminalSnapshot("after eof", term)).toMatchSnapshot(); + + await expect(vm.waitShell(shellId)).resolves.toBe(0); + }, 60_000); +}); diff --git a/packages/core/tests/shell-flat-api.test.ts b/packages/core/tests/shell-flat-api.test.ts index d5cc943d5..594ea54a8 100644 --- a/packages/core/tests/shell-flat-api.test.ts +++ b/packages/core/tests/shell-flat-api.test.ts @@ -1,6 +1,10 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { AgentOs } from "../src/index.js"; +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + describe("flat shell API", () => { let vm: AgentOs; @@ -39,4 +43,23 @@ describe("flat shell API", () => { const output = chunks.join(""); expect(output).toContain("hello-flat-shell"); }, 30_000); + + test("default shell echoes typed characters before newline", async () => { + const { shellId } = vm.openShell(); + + const chunks: string[] = []; + vm.onShellData(shellId, (data) => { + chunks.push(new TextDecoder().decode(data)); + }); + + await sleep(100); + vm.writeShell(shellId, "abc"); + await sleep(100); + + vm.closeShell(shellId); + + const output = chunks.join(""); + expect(output).toContain("sh-0.4$ "); + expect(output).toContain("abc"); + }, 30_000); }); diff --git a/packages/dev-shell/.pi/agent/auth.json b/packages/dev-shell/.pi/agent/auth.json deleted file mode 100644 index 9e26dfeeb..000000000 --- a/packages/dev-shell/.pi/agent/auth.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/packages/dev-shell/package.json b/packages/dev-shell/package.json deleted file mode 100644 index 76002c85a..000000000 --- a/packages/dev-shell/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@rivet-dev/agentos-dev-shell", - "private": true, - "type": "module", - "bin": { - "agentos-dev-shell": "./dist/shell.js" - }, - "scripts": { - "build": "tsc", - "check-types": "tsc --noEmit", - "dev-shell": "pnpm exec tsx src/shell.ts", - "test": "NODE_OPTIONS=--max-old-space-size=256 pnpm exec vitest run --fileParallelism=false --reporter=verbose" - }, - "dependencies": { - "@agentos-software/common": "catalog:", - "@rivet-dev/agentos-core": "workspace:*", - "pino": "^10.3.1" - }, - "devDependencies": { - "@types/node": "^22.19.3", - "@xterm/headless": "^6.0.0", - "tsx": "^4.19.2", - "typescript": "^5.7.2", - "vitest": "^2.1.8" - } -} diff --git a/packages/dev-shell/src/debug-logger.ts b/packages/dev-shell/src/debug-logger.ts deleted file mode 100644 index 40040be60..000000000 --- a/packages/dev-shell/src/debug-logger.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { createWriteStream, type WriteStream } from "node:fs"; -import pino from "pino"; - -/** Keys whose values are redacted in debug log records. */ -const REDACT_KEYS = [ - "ANTHROPIC_API_KEY", - "OPENAI_API_KEY", - "API_KEY", - "SECRET", - "TOKEN", - "PASSWORD", - "CREDENTIAL", - "Authorization", -]; - -export interface DebugLogger extends pino.Logger { - /** Flush and close the underlying file stream. */ - close(): Promise; -} - -/** - * Create a structured pino logger that writes JSON lines to `filePath`. - * - * The logger never writes to stdout/stderr — all output goes exclusively - * to the file sink so it cannot contaminate PTY rendering or protocol output. - * - * Secrets are redacted by key name via pino's built-in redact paths. - */ -export function createDebugLogger(filePath: string): DebugLogger { - const fileStream: WriteStream = createWriteStream(filePath, { flags: "a" }); - - const redactPaths = REDACT_KEYS.flatMap((key) => [ - key, - `env.${key}`, - `*.${key}`, - ]); - - const logger = pino( - { - level: "trace", - timestamp: pino.stdTimeFunctions.isoTime, - redact: { - paths: redactPaths, - censor: "[REDACTED]", - }, - }, - fileStream, - ) as pino.Logger & { close: () => Promise }; - - logger.close = () => - new Promise((resolve, reject) => { - fileStream.end(() => { - fileStream.close((err) => { - if (err) reject(err); - else resolve(); - }); - }); - }); - - return logger; -} - -/** - * Return a no-op logger that satisfies the DebugLogger interface but - * discards all records. Used when no debug log path is configured. - */ -export function createNoopLogger(): DebugLogger { - const logger = pino({ level: "silent" }) as pino.Logger & { - close: () => Promise; - }; - logger.close = () => Promise.resolve(); - return logger; -} diff --git a/packages/dev-shell/src/index.ts b/packages/dev-shell/src/index.ts deleted file mode 100644 index 0cfbc646e..000000000 --- a/packages/dev-shell/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type { DebugLogger } from "./debug-logger.js"; -export { createDebugLogger, createNoopLogger } from "./debug-logger.js"; -export type { DevShellKernelResult, DevShellOptions } from "./kernel.js"; -export { createDevShellKernel } from "./kernel.js"; -export { collectShellEnv, resolveWorkspacePaths } from "./shared.js"; diff --git a/packages/dev-shell/src/kernel.ts b/packages/dev-shell/src/kernel.ts deleted file mode 100644 index b950c1260..000000000 --- a/packages/dev-shell/src/kernel.ts +++ /dev/null @@ -1,744 +0,0 @@ -import type { Stats } from "node:fs"; -import { existsSync } from "node:fs"; -import * as fsPromises from "node:fs/promises"; -import { createRequire } from "node:module"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import type { - Kernel, - ManagedProcess, - ShellHandle, - VirtualFileSystem, -} from "@rivet-dev/agentos-core/internal/runtime-compat"; -import * as runtimeCompat from "@rivet-dev/agentos-core/internal/runtime-compat"; -import type { DebugLogger } from "./debug-logger.js"; -import { createDebugLogger, createNoopLogger } from "./debug-logger.js"; -import type { WorkspacePaths } from "./shared.js"; -import { collectShellEnv, resolveWorkspacePaths } from "./shared.js"; - -const moduleDir = path.dirname(fileURLToPath(import.meta.url)); -const moduleRequire = createRequire(import.meta.url); -const DEV_SHELL_TMP_ROOT_PREFIX = `agentos-dev-shell-${process.pid}-`; -export interface DevShellOptions { - workDir?: string; - mountWasm?: boolean; - envFilePath?: string; - /** When set, structured pino debug logs are written to this file path. */ - debugLogPath?: string; -} - -export interface DevShellKernelResult { - kernel: Kernel; - workDir: string; - env: Record; - loadedCommands: string[]; - paths: WorkspacePaths; - logger: DebugLogger; - dispose: () => Promise; -} - -async function createSessionTmpRoot(): Promise<{ - rootDir: string; - tmpDir: string; -}> { - const rootDir = await fsPromises.mkdtemp( - path.join(tmpdir(), DEV_SHELL_TMP_ROOT_PREFIX), - ); - const tmpDir = path.join(rootDir, "tmp"); - await fsPromises.mkdir(tmpDir, { recursive: true }); - return { rootDir, tmpDir }; -} - -function isWithinVirtualPath(targetPath: string, prefix: string): boolean { - const normalizedTarget = path.posix.normalize(targetPath); - const normalizedPrefix = path.posix.normalize(prefix); - return ( - normalizedTarget === normalizedPrefix || - normalizedTarget.startsWith(`${normalizedPrefix}/`) - ); -} - -function resolvePiCliPath(paths: WorkspacePaths): string | undefined { - try { - return moduleRequire.resolve("@mariozechner/pi-coding-agent/dist/cli.js"); - } catch { - const candidates = [ - path.join( - paths.hostProjectRoot, - "node_modules", - "@mariozechner", - "pi-coding-agent", - "dist", - "cli.js", - ), - path.join( - paths.workspaceRoot, - "registry", - "agent", - "pi", - "node_modules", - "@mariozechner", - "pi-coding-agent", - "dist", - "cli.js", - ), - path.join( - paths.workspaceRoot, - "packages", - "core", - "node_modules", - "@mariozechner", - "pi-coding-agent", - "dist", - "cli.js", - ), - ]; - - return candidates.find((candidate) => existsSync(candidate)); - } -} - -function prepareKernelInvocation( - command: string, - args: string[], - piCliPath: string | undefined, -): { - command: string; - args: string[]; - driver: string; - cwd?: string; - env?: Record; - execCommand?: string; -} { - if (command === "pi" && piCliPath) { - if (args.includes("--help") || args.includes("-h")) { - return { - command: "node", - args: [ - "-e", - [ - 'process.stdout.write("Usage: pi [options] [prompt]\\n");', - 'process.stdout.write("pi dev-shell shim: only --help is supported in this runtime path today.\\n");', - ].join("\n"), - ], - driver: "node", - }; - } - - return { - command: "node", - args: [ - "-e", - [ - "process.stderr.write(", - ' "pi dev-shell shim: only --help is currently supported in the sandbox-native dev shell.\\n",', - ");", - "process.exit(1);", - ].join("\n"), - ], - driver: "node", - }; - } - - if (command === "node" && args[0] === "-e") { - return { - command: "node", - args: [ - "-e", - [ - "const __agentOsFormat = (value) => {", - ' if (typeof value === "string") return value;', - " try {", - " return typeof value === 'object' ? JSON.stringify(value) : String(value);", - " } catch {", - " return String(value);", - " }", - "};", - "const __agentOsWrite = (stream, values) => {", - " stream.write(values.map(__agentOsFormat).join(' ') + '\\n');", - "};", - "globalThis.console = {", - " ...(globalThis.console ?? {}),", - " log: (...values) => __agentOsWrite(process.stdout, values),", - " info: (...values) => __agentOsWrite(process.stdout, values),", - " warn: (...values) => __agentOsWrite(process.stderr, values),", - " error: (...values) => __agentOsWrite(process.stderr, values),", - "};", - "(async () => {", - args[1] ?? "", - "})().catch((error) => {", - " process.stderr.write(", - ' String(error && error.stack ? error.stack : error) + "\\n",', - " );", - " process.exit(1);", - "});", - ].join("\n"), - ], - driver: "node", - }; - } - - if (command === "node" && args[0] === "--version") { - return { - command: "node", - args: ["-e", 'process.stdout.write(String(process.version) + "\\n");'], - driver: "node", - }; - } - - if ( - (command === "bash" || command === "sh") && - (args[0] === "-c" || args[0] === "-lc" || args[0] === "-ic") && - args.length === 2 - ) { - return { - command, - args, - driver: `${command}:exec`, - }; - } - - return { - command, - args, - driver: command, - }; -} - -function wrapManagedProcess( - process: ManagedProcess, - logger: DebugLogger, - logFields: Record, -): ManagedProcess { - let waitPromise: Promise | null = null; - - return { - pid: process.pid, - writeStdin(data) { - process.writeStdin(data); - }, - closeStdin() { - process.closeStdin(); - }, - kill(signal) { - process.kill(signal); - }, - wait() { - if (waitPromise !== null) { - return waitPromise; - } - waitPromise = process.wait().then((exitCode) => { - logger.info({ ...logFields, exitCode }, "process exited"); - return exitCode; - }); - return waitPromise; - }, - get exitCode() { - return process.exitCode; - }, - }; -} - -function classifySpawnFailure(error: unknown): { - exitCode: number; - stderr: string; -} { - const message = error instanceof Error ? error.message : String(error); - const lowerMessage = message.toLowerCase(); - const exitCode = - message.includes("EACCES") || lowerMessage.includes("permission denied") - ? 126 - : 127; - return { - exitCode, - stderr: message.endsWith("\n") ? message : `${message}\n`, - }; -} - -function createFailedManagedProcess( - pid: number, - exitCode: number, -): ManagedProcess { - return { - pid, - writeStdin() {}, - closeStdin() {}, - kill() {}, - wait() { - return Promise.resolve(exitCode); - }, - get exitCode() { - return exitCode; - }, - }; -} - -function wrapShellHandle( - handle: ShellHandle, - logger: DebugLogger, - logFields: Record, -): ShellHandle { - let waitPromise: Promise | null = null; - let onData: ((data: Uint8Array) => void) | null = null; - const pendingOutput: Uint8Array[] = []; - - handle.onData = (data) => { - if (onData) { - onData(data); - return; - } - pendingOutput.push(data); - }; - - return { - pid: handle.pid, - write(data) { - handle.write(data); - }, - get onData() { - return onData; - }, - set onData(value) { - onData = value; - if (!value || pendingOutput.length === 0) { - return; - } - for (const chunk of pendingOutput.splice(0)) { - value(chunk); - } - }, - resize(cols, rows) { - logger.info({ ...logFields, cols, rows }, "pty resized"); - handle.resize(cols, rows); - }, - kill(signal) { - handle.kill(signal); - }, - wait() { - if (waitPromise !== null) { - return waitPromise; - } - waitPromise = handle.wait().then((exitCode) => { - logger.info({ ...logFields, exitCode }, "pty exited"); - return exitCode; - }); - return waitPromise; - }, - }; -} - -function wrapKernel( - kernel: Kernel, - logger: DebugLogger, - piCliPath: string | undefined, -): Kernel { - const commands = new Map(kernel.commands); - let syntheticFailurePid = 1_000_000; - if (piCliPath) { - commands.set("pi", "node"); - } - - const wrappedKernel = Object.create(kernel) as Kernel; - Object.assign(wrappedKernel, { - commands, - spawn( - command: string, - args: string[], - options?: Parameters[2], - ) { - const translated = prepareKernelInvocation(command, args, piCliPath); - try { - if ( - translated.execCommand !== undefined && - options?.streamStdin !== true && - options?.stdinFd === undefined && - options?.stdoutFd === undefined && - options?.stderrFd === undefined - ) { - const execCommand = translated.execCommand; - const pid = syntheticFailurePid++; - let exitCode: number | null = null; - let waitPromise: Promise | null = null; - const execOptions = { - cwd: translated.cwd ?? options?.cwd, - env: { - ...(options?.env ?? {}), - ...(translated.env ?? {}), - }, - }; - const logFields = { - pid, - command, - args, - driver: translated.driver, - cwd: options?.cwd, - }; - logger.info(logFields, "process spawned"); - return wrapManagedProcess( - { - pid, - writeStdin() {}, - closeStdin() {}, - kill() {}, - wait() { - if (waitPromise !== null) { - return waitPromise; - } - waitPromise = wrappedKernel - .exec(execCommand, execOptions) - .then((result) => { - if (result.stdout.length > 0) { - options?.onStdout?.(Buffer.from(result.stdout, "utf8")); - } - if (result.stderr.length > 0) { - options?.onStderr?.(Buffer.from(result.stderr, "utf8")); - } - exitCode = result.exitCode; - return result.exitCode; - }) - .catch((error) => { - const failure = classifySpawnFailure(error); - if (failure.stderr.length > 0) { - options?.onStderr?.(Buffer.from(failure.stderr, "utf8")); - } - exitCode = failure.exitCode; - return failure.exitCode; - }); - return waitPromise; - }, - get exitCode() { - return exitCode; - }, - }, - logger, - logFields, - ); - } - - const process = kernel.spawn(translated.command, translated.args, { - ...options, - cwd: translated.cwd ?? options?.cwd, - env: { - ...(options?.env ?? {}), - ...(translated.env ?? {}), - }, - }); - const logFields = { - pid: process.pid, - command, - args, - driver: translated.driver, - cwd: options?.cwd, - }; - logger.info(logFields, "process spawned"); - return wrapManagedProcess(process, logger, logFields); - } catch (error) { - const failurePid = syntheticFailurePid++; - const { exitCode, stderr } = classifySpawnFailure(error); - if (stderr.length > 0) { - options?.onStderr?.(Buffer.from(stderr, "utf8")); - } - logger.info( - { - pid: failurePid, - command, - args, - driver: translated.driver, - cwd: options?.cwd, - exitCode, - error: stderr.trimEnd(), - }, - "process spawn rejected", - ); - return createFailedManagedProcess(failurePid, exitCode); - } - }, - openShell(options?: Parameters[0]) { - const requestedCommand = options?.command ?? "sh"; - const requestedArgs = - options?.args ?? - (requestedCommand === "bash" || requestedCommand === "sh" - ? ["-i"] - : []); - const translated = prepareKernelInvocation( - requestedCommand, - requestedArgs, - piCliPath, - ); - const handle = kernel.openShell({ - ...options, - command: translated.command, - args: translated.args, - cwd: translated.cwd ?? options?.cwd, - env: { - ...(options?.env ?? {}), - ...(translated.env ?? {}), - }, - }); - const logFields = { - pid: handle.pid, - command: requestedCommand, - args: requestedArgs, - driver: translated.driver, - cwd: options?.cwd, - cols: options?.cols, - rows: options?.rows, - }; - logger.info(logFields, "pty opened"); - return wrapShellHandle(handle, logger, logFields); - }, - async connectTerminal(options?: Parameters[0]) { - const requestedCommand = options?.command ?? "sh"; - const requestedArgs = - options?.args ?? - (requestedCommand === "bash" || requestedCommand === "sh" - ? ["-i"] - : []); - const translated = prepareKernelInvocation( - requestedCommand, - requestedArgs, - piCliPath, - ); - logger.info( - { - command: requestedCommand, - args: requestedArgs, - driver: translated.driver, - cwd: options?.cwd, - cols: options?.cols, - rows: options?.rows, - }, - "pty connected", - ); - const exitCode = await kernel.connectTerminal({ - ...options, - command: translated.command, - args: translated.args, - cwd: translated.cwd ?? options?.cwd, - env: { - ...(options?.env ?? {}), - ...(translated.env ?? {}), - }, - }); - logger.info( - { - command: requestedCommand, - args: requestedArgs, - driver: translated.driver, - exitCode, - }, - "pty exited", - ); - return exitCode; - }, - }); - - return wrappedKernel; -} - -async function seedFilesystemFromHost( - filesystem: VirtualFileSystem, - hostPath: string, - guestPath: string, -): Promise { - let stats: Stats; - try { - stats = await fsPromises.lstat(hostPath); - } catch (error) { - const code = - typeof error === "object" && error !== null && "code" in error - ? String((error as { code?: unknown }).code) - : undefined; - if (code === "ENOENT") { - await filesystem.mkdir(guestPath, { recursive: true }); - return; - } - throw error; - } - - if (stats.isSymbolicLink()) { - const linkTarget = await fsPromises.readlink(hostPath); - await filesystem.symlink(linkTarget, guestPath); - return; - } - - if (stats.isDirectory()) { - await filesystem.mkdir(guestPath, { recursive: true }); - const entries = await fsPromises.readdir(hostPath, { withFileTypes: true }); - for (const entry of entries) { - await seedFilesystemFromHost( - filesystem, - path.join(hostPath, entry.name), - path.posix.join(guestPath, entry.name), - ); - } - await filesystem.chmod(guestPath, stats.mode & 0o7777); - return; - } - - const fileBytes = await fsPromises.readFile(hostPath); - await filesystem.writeFile(guestPath, fileBytes); - await filesystem.chmod(guestPath, stats.mode & 0o7777); -} - -export async function createDevShellKernel( - options: DevShellOptions = {}, -): Promise { - const paths = resolveWorkspacePaths(moduleDir); - const workDir = path.resolve(options.workDir ?? process.cwd()); - const mountWasm = options.mountWasm !== false; - const sessionTmpRoot = await createSessionTmpRoot(); - const env = collectShellEnv(options.envFilePath ?? paths.realProviderEnvFile); - if (!process.env.AGENT_OS_NODE_BINARY) { - process.env.AGENT_OS_NODE_BINARY = process.execPath; - } - - // Set up structured debug logger (file-only, never stdout/stderr). - const logger = options.debugLogPath - ? createDebugLogger(options.debugLogPath) - : createNoopLogger(); - logger.info({ workDir, mountWasm }, "dev-shell session init"); - env.HOME = workDir; - env.XDG_CONFIG_HOME = path.join(workDir, ".config"); - env.XDG_CACHE_HOME = path.join(workDir, ".cache"); - env.XDG_DATA_HOME = path.join(workDir, ".local", "share"); - env.HISTFILE = "/dev/null"; - env.PATH = "/bin"; - env.TMPDIR = "/tmp"; - env.TMP = "/tmp"; - env.TEMP = "/tmp"; - if (!env.AGENT_OS_NODE_BINARY) { - env.AGENT_OS_NODE_BINARY = process.execPath; - } - - const piCliPath = resolvePiCliPath(paths); - const filesystem = runtimeCompat.createInMemoryFileSystem(); - try { - await seedFilesystemFromHost(filesystem, workDir, workDir); - await filesystem.mkdir("/tmp", { recursive: true }); - await filesystem.mkdir(env.XDG_CONFIG_HOME, { recursive: true }); - await filesystem.mkdir(env.XDG_CACHE_HOME, { recursive: true }); - await filesystem.mkdir(env.XDG_DATA_HOME, { recursive: true }); - - const sessionTmpFileSystem = new runtimeCompat.NodeFileSystem({ - root: sessionTmpRoot.tmpDir, - }); - if (workDir.startsWith("/tmp/")) { - const workDirInTmpMount = workDir.slice("/tmp".length); - await seedFilesystemFromHost( - sessionTmpFileSystem, - workDir, - workDirInTmpMount, - ); - if (isWithinVirtualPath(env.XDG_CONFIG_HOME, workDir)) { - await sessionTmpFileSystem.mkdir( - env.XDG_CONFIG_HOME.slice("/tmp".length), - { - recursive: true, - }, - ); - } - if (isWithinVirtualPath(env.XDG_CACHE_HOME, workDir)) { - await sessionTmpFileSystem.mkdir( - env.XDG_CACHE_HOME.slice("/tmp".length), - { - recursive: true, - }, - ); - } - if (isWithinVirtualPath(env.XDG_DATA_HOME, workDir)) { - await sessionTmpFileSystem.mkdir( - env.XDG_DATA_HOME.slice("/tmp".length), - { - recursive: true, - }, - ); - } - } - - const mounts: Array<{ - path: string; - fs: VirtualFileSystem; - }> = [ - { - path: "/tmp", - fs: sessionTmpFileSystem, - }, - ]; - - const kernel = runtimeCompat.createKernel({ - filesystem, - hostNetworkAdapter: runtimeCompat.createNodeHostNetworkAdapter(), - permissions: runtimeCompat.allowAll, - env, - cwd: workDir, - logger, - mounts, - syncFilesystemOnDispose: false, - }); - - const loadedCommands: string[] = []; - - if (mountWasm) { - const wasmRuntime = runtimeCompat.createWasmVmRuntime({ - commandDirs: paths.wasmCommandDirs, - }); - await kernel.mount(wasmRuntime); - loadedCommands.push(...wasmRuntime.commands); - logger.info( - { driver: wasmRuntime.name, commands: wasmRuntime.commands }, - "runtime driver mounted", - ); - } - - const nodeRuntime = runtimeCompat.createNodeRuntime(); - await kernel.mount(nodeRuntime); - loadedCommands.push(...nodeRuntime.commands); - logger.info( - { driver: nodeRuntime.name, commands: nodeRuntime.commands }, - "runtime driver mounted", - ); - - if (piCliPath) { - loadedCommands.push("pi"); - logger.info({ command: "pi", piCliPath }, "runtime driver mounted"); - } - - const filteredCommands = Array.from(new Set(loadedCommands)) - .filter( - (command) => command.trim().length > 0 && !command.startsWith("_"), - ) - .sort(); - logger.info({ loadedCommands: filteredCommands }, "dev-shell ready"); - const wrappedKernel = wrapKernel(kernel, logger, piCliPath); - - return { - kernel: wrappedKernel, - workDir, - env, - loadedCommands: filteredCommands, - paths, - logger, - dispose: async () => { - logger.info("dev-shell disposing"); - try { - await kernel.dispose(); - } finally { - await fsPromises.rm(sessionTmpRoot.rootDir, { - recursive: true, - force: true, - }); - await logger.close(); - } - }, - }; - } catch (error) { - await fsPromises.rm(sessionTmpRoot.rootDir, { - recursive: true, - force: true, - }); - await logger.close(); - throw error; - } -} diff --git a/packages/dev-shell/src/shared.ts b/packages/dev-shell/src/shared.ts deleted file mode 100644 index 60bc83fc5..000000000 --- a/packages/dev-shell/src/shared.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { existsSync, readFileSync } from "node:fs"; -import { homedir } from "node:os"; -import path from "node:path"; -import commonSoftware from "@agentos-software/common"; - -export interface WorkspacePaths { - workspaceRoot: string; - hostProjectRoot: string; - wasmCommandsDir: string; - wasmCommandDirs: string[]; - realProviderEnvFile: string; -} - -interface WasmCommandDescriptor { - commandDir?: unknown; -} - -function collectCommandDirs( - input: unknown, - commandDirs: string[] = [], -): string[] { - if (Array.isArray(input)) { - for (const item of input) { - collectCommandDirs(item, commandDirs); - } - return commandDirs; - } - - if (input && typeof input === "object") { - const commandDir = (input as WasmCommandDescriptor).commandDir; - if (typeof commandDir === "string") { - commandDirs.push(commandDir); - } - } - - return commandDirs; -} - -export function findWorkspaceRoot(startDir: string): string { - let current = path.resolve(startDir); - - while (true) { - if (existsSync(path.join(current, "pnpm-workspace.yaml"))) { - return current; - } - - const parent = path.dirname(current); - if (parent === current) { - throw new Error(`Could not locate pnpm-workspace.yaml from ${startDir}`); - } - current = parent; - } -} - -export function resolveWorkspacePaths(startDir: string): WorkspacePaths { - const workspaceRoot = findWorkspaceRoot(startDir); - const builtWasmCommandsDir = path.join( - workspaceRoot, - "registry", - "native", - "target", - "wasm32-wasip1", - "release", - "commands", - ); - const packagedCoreutilsWasmDir = path.join( - workspaceRoot, - "registry", - "software", - "coreutils", - "wasm", - ); - const packagedCommonCommandDirs = collectCommandDirs(commonSoftware); - const wasmCommandDirs = [ - ...packagedCommonCommandDirs, - builtWasmCommandsDir, - packagedCoreutilsWasmDir, - ].filter((commandDir, index, allDirs) => { - return existsSync(commandDir) && allDirs.indexOf(commandDir) === index; - }); - return { - workspaceRoot, - // Dev-shell used to live in a nested runtime repo. In this monorepo, - // the workspace root itself is the host-visible project root. - hostProjectRoot: workspaceRoot, - wasmCommandsDir: wasmCommandDirs[0] ?? packagedCoreutilsWasmDir, - wasmCommandDirs, - realProviderEnvFile: path.join(homedir(), "misc", "env.txt"), - }; -} - -export function stripWrappingQuotes(value: string): string { - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - return value.slice(1, -1); - } - return value; -} - -export function parseEnvFile(filePath: string): Record { - const parsed: Record = {}; - const contents = readFileSync(filePath, "utf8"); - - for (const rawLine of contents.split(/\r?\n/)) { - const line = rawLine.trim(); - if (!line || line.startsWith("#")) continue; - - const withoutExport = line.startsWith("export ") - ? line.slice("export ".length).trim() - : line; - const separator = withoutExport.indexOf("="); - if (separator <= 0) continue; - - const key = withoutExport.slice(0, separator).trim(); - const rawValue = withoutExport.slice(separator + 1).trim(); - if (!key) continue; - - parsed[key] = stripWrappingQuotes(rawValue); - } - - return parsed; -} - -export function collectShellEnv(envFilePath?: string): Record { - const shellEnv: Record = {}; - - for (const [key, value] of Object.entries(process.env)) { - if (typeof value === "string") { - shellEnv[key] = value; - } - } - - const sourcePath = envFilePath ?? path.join(homedir(), "misc", "env.txt"); - if (existsSync(sourcePath)) { - for (const [key, value] of Object.entries(parseEnvFile(sourcePath))) { - if (!(key in shellEnv)) { - shellEnv[key] = value; - } - } - } - - if (!shellEnv.TERM) shellEnv.TERM = "xterm-256color"; - if (!shellEnv.COLORTERM) shellEnv.COLORTERM = "truecolor"; - - return shellEnv; -} diff --git a/packages/dev-shell/src/shell.ts b/packages/dev-shell/src/shell.ts deleted file mode 100644 index f3c12a2e3..000000000 --- a/packages/dev-shell/src/shell.ts +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env node - -import path from "node:path"; -import { createDevShellKernel } from "./kernel.js"; - -interface CliOptions { - workDir?: string; - debugLogPath?: string; - mountWasm: boolean; - command: string; - args: string[]; -} - -function printUsage(): void { - console.error( - [ - "Usage:", - " agentos-dev-shell [--work-dir ] [--debug-log ] [--no-wasm] [--] [command] [args...]", - "", - "Examples:", - " just dev-shell", - " just dev-shell --work-dir /tmp/demo", - " just dev-shell --debug-log /tmp/dev-shell-debug.ndjson", - " just dev-shell sh", - " just dev-shell -- node -e 'console.log(process.version)'", - ].join("\n"), - ); -} - -function shQuote(value: string): string { - return `'${value.replace(/'/g, "'\\''")}'`; -} - -function parseArgs(argv: string[]): CliOptions { - const normalizedArgv = argv[0] === "--" ? argv.slice(1) : argv; - const options: CliOptions = { - mountWasm: true, - command: "bash", - args: [], - }; - - for (let index = 0; index < normalizedArgv.length; index++) { - const arg = normalizedArgv[index]; - if (arg === "--") { - const trailing = normalizedArgv.slice(index + 1); - if (trailing.length > 0) { - options.command = trailing[0]; - options.args = trailing.slice(1); - } - break; - } - - if (!arg.startsWith("-")) { - options.command = arg; - options.args = normalizedArgv.slice(index + 1); - break; - } - - switch (arg) { - case "--work-dir": - if (!normalizedArgv[index + 1]) { - throw new Error("--work-dir requires a path"); - } - options.workDir = path.resolve(normalizedArgv[++index]); - break; - case "--debug-log": - if (!normalizedArgv[index + 1]) { - throw new Error("--debug-log requires a file path"); - } - options.debugLogPath = path.resolve(normalizedArgv[++index]); - break; - case "--no-wasm": - options.mountWasm = false; - break; - case "--help": - case "-h": - printUsage(); - process.exit(0); - return options; - default: - throw new Error(`Unknown argument: ${arg}`); - } - } - - return options; -} - -const cli = parseArgs(process.argv.slice(2)); -if (!cli.mountWasm && (cli.command === "bash" || cli.command === "sh")) { - throw new Error( - "Interactive dev-shell requires WasmVM for the shell process; remove --no-wasm.", - ); -} - -const shell = await createDevShellKernel({ - workDir: cli.workDir, - mountWasm: cli.mountWasm, - debugLogPath: cli.debugLogPath, -}); - -console.error(`agent-os dev shell`); -console.error(`work dir: ${shell.workDir}`); -console.error(`loaded commands: ${shell.loadedCommands.join(", ")}`); - -const terminalCommand = - cli.command === "bash" || cli.command === "sh" - ? (() => { - if (cli.args.length === 0) { - return { - command: cli.command, - args: [], - }; - } - - if ( - (cli.args[0] === "-c" || cli.args[0] === "-lc") && - cli.args.length >= 2 - ) { - return { - command: cli.command, - args: [ - cli.args[0], - `cd ${shQuote(shell.workDir)} && ${cli.args[1]}`, - ...cli.args.slice(2), - ], - }; - } - - return { - command: cli.command, - args: cli.args, - }; - })() - : { - command: cli.command, - args: cli.args, - }; - -const exitCode = - (terminalCommand.command === "bash" || terminalCommand.command === "sh") && - terminalCommand.args.length === 0 - ? await shell.kernel.connectTerminal({ - command: terminalCommand.command, - args: terminalCommand.args, - cwd: shell.workDir, - env: shell.env, - }) - : await new Promise((resolve) => { - const proc = shell.kernel.spawn( - terminalCommand.command, - terminalCommand.args, - { - cwd: shell.workDir, - env: shell.env, - onStdout: (data) => { - process.stdout.write(Buffer.from(data)); - }, - onStderr: (data) => { - process.stderr.write(Buffer.from(data)); - }, - }, - ); - void proc.wait().then(resolve); - }); - -await shell.dispose(); -process.exit(exitCode); diff --git a/packages/dev-shell/test/dev-shell-cli.integration.test.ts b/packages/dev-shell/test/dev-shell-cli.integration.test.ts deleted file mode 100644 index bbd1d062f..000000000 --- a/packages/dev-shell/test/dev-shell-cli.integration.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { spawn } from "node:child_process"; -import { existsSync, readFileSync } from "node:fs"; -import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { afterEach, describe, expect, it } from "vitest"; -import { resolveWorkspacePaths } from "../src/shared.ts"; - -interface CommandResult { - exitCode: number; - stdout: string; - stderr: string; -} - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const workspaceRoot = path.resolve(__dirname, "..", "..", ".."); -const justfilePath = path.join(workspaceRoot, "justfile"); -const fallbackRecipe = - 'pnpm --filter @rivet-dev/agentos-dev-shell dev-shell -- "$@"'; -resolveWorkspacePaths(__dirname); - -function resolveExecutable(binaryName: string): string | undefined { - const pathValue = process.env.PATH; - if (!pathValue) { - return undefined; - } - - const candidateNames = - process.platform === "win32" - ? [binaryName, `${binaryName}.exe`, `${binaryName}.cmd`] - : [binaryName]; - - for (const entry of pathValue.split(path.delimiter)) { - if (!entry) { - continue; - } - - for (const candidateName of candidateNames) { - const candidatePath = path.join(entry, candidateName); - if (existsSync(candidatePath)) { - return candidatePath; - } - } - } - - return undefined; -} - -function createDevShellWrapperProcess(args: string[]) { - const justBinary = resolveExecutable("just"); - if (justBinary) { - return spawn( - justBinary, - ["--justfile", justfilePath, "dev-shell", ...args], - { - cwd: workspaceRoot, - env: process.env, - stdio: ["ignore", "pipe", "pipe"], - }, - ); - } - - const justfileContents = readFileSync(justfilePath, "utf8"); - if (!justfileContents.includes(fallbackRecipe)) { - throw new Error( - "just is not installed and the dev-shell justfile recipe no longer matches the pnpm fallback command", - ); - } - - const separatorIndex = args.indexOf("--"); - const forwardedArgs = - separatorIndex === -1 - ? args - : [...args.slice(0, separatorIndex), ...args.slice(separatorIndex + 1)]; - return spawn( - "pnpm", - [ - "--filter", - "@rivet-dev/agentos-dev-shell", - "dev-shell", - "--", - ...forwardedArgs, - ], - { - cwd: workspaceRoot, - env: process.env, - stdio: ["ignore", "pipe", "pipe"], - }, - ); -} - -function stripJustPreamble(output: string): string { - return output - .split("\n") - .filter( - (line) => - line.length > 0 && - !line.startsWith( - "pnpm --filter @rivet-dev/agentos-dev-shell dev-shell --", - ) && - !line.startsWith("> @rivet-dev/agentos-dev-shell@ dev-shell ") && - !line.startsWith("> pnpm exec tsx src/shell.ts ") && - !line.startsWith("> tsx src/shell.ts ") && - !line.startsWith( - "> node ../../node_modules/tsx/dist/cli.mjs src/shell.ts ", - ), - ) - .join("\n") - .trim(); -} - -function runJustDevShell( - args: string[], - timeoutMs = 30_000, -): Promise { - return new Promise((resolve, reject) => { - const child = createDevShellWrapperProcess(args); - - const stdoutChunks: Buffer[] = []; - const stderrChunks: Buffer[] = []; - const timer = setTimeout(() => { - child.kill("SIGKILL"); - reject(new Error(`Timed out running: just dev-shell ${args.join(" ")}`)); - }, timeoutMs); - - child.stdout.on("data", (chunk: Buffer) => stdoutChunks.push(chunk)); - child.stderr.on("data", (chunk: Buffer) => stderrChunks.push(chunk)); - child.on("error", (error) => { - clearTimeout(timer); - reject(error); - }); - child.on("close", (code) => { - clearTimeout(timer); - resolve({ - exitCode: code ?? 1, - stdout: Buffer.concat(stdoutChunks).toString("utf8"), - stderr: Buffer.concat(stderrChunks).toString("utf8"), - }); - }); - }); -} - -describe("dev-shell justfile wrapper", { timeout: 60_000 }, () => { - let workDir: string | undefined; - - afterEach(async () => { - if (workDir) { - await rm(workDir, { recursive: true, force: true }); - workDir = undefined; - } - }); - - it("runs the default work dir through the just wrapper", async () => { - const result = await runJustDevShell([ - "--", - "node", - "-e", - "process.stdout.write(process.cwd())", - ]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toContain("agent-os dev shell"); - expect(result.stderr).toContain("loaded commands:"); - expect(stripJustPreamble(result.stdout)).toBe( - path.resolve(workspaceRoot, "packages", "dev-shell"), - ); - }); - - it("passes --work-dir through the just wrapper", async () => { - workDir = await mkdtemp(path.join(tmpdir(), "agentos-dev-shell-just-")); - const result = await runJustDevShell([ - "--work-dir", - workDir, - "--", - "node", - "-e", - "process.stdout.write(process.cwd())", - ]); - expect(result.exitCode).toBe(0); - expect(result.stderr).toContain(`work dir: ${workDir}`); - expect(stripJustPreamble(result.stdout)).toBe(workDir); - }); - - it("runs startup commands through the just wrapper", async () => { - const result = await runJustDevShell([ - "--", - "node", - "-e", - "console.log('JUST_DEV_SHELL_NODE_OK')", - ]); - expect(result.exitCode).toBe(0); - expect(stripJustPreamble(result.stdout)).toContain( - "JUST_DEV_SHELL_NODE_OK", - ); - }); - - it("runs scripted shell commands through the just wrapper", async () => { - workDir = await mkdtemp(path.join(tmpdir(), "agentos-dev-shell-shell-")); - const shellWorkDir = workDir; - const result = await runJustDevShell([ - "--work-dir", - shellWorkDir, - "--", - "bash", - "-lc", - `echo cli-shell-ok && pwd`, - ]); - expect(result.exitCode).toBe(0); - const stdout = stripJustPreamble(result.stdout); - expect(stdout).toContain("cli-shell-ok"); - expect(stdout).toContain(shellWorkDir); - }); - - it("runs pi through the just wrapper", async () => { - const result = await runJustDevShell(["--", "pi", "--help"], 45_000); - expect(result.exitCode).toBe(0); - expect(`${result.stdout}\n${result.stderr}`).toMatch(/pi|usage|Usage/); - }); -}); diff --git a/packages/dev-shell/test/dev-shell.integration.test.ts b/packages/dev-shell/test/dev-shell.integration.test.ts deleted file mode 100644 index 6f7c0b3cc..000000000 --- a/packages/dev-shell/test/dev-shell.integration.test.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { existsSync } from "node:fs"; -import { - chmod, - mkdtemp, - readdir, - readFile, - rm, - writeFile, -} from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; -import { createDevShellKernel } from "../src/index.ts"; - -const DEV_SHELL_TMP_ROOT_PREFIX = `agentos-dev-shell-${process.pid}-`; -type StreamWrite = (chunk: unknown, ...rest: unknown[]) => unknown; - -async function listDevShellTempRoots(): Promise { - return (await readdir(tmpdir(), { withFileTypes: true })) - .filter( - (entry) => - entry.isDirectory() && entry.name.startsWith(DEV_SHELL_TMP_ROOT_PREFIX), - ) - .map((entry) => path.join(tmpdir(), entry.name)) - .sort(); -} - -async function runKernelCommand( - shell: Awaited>, - command: string, - args: string[], - timeoutMs = 20_000, -): Promise<{ exitCode: number; stdout: string; stderr: string }> { - let stdout = ""; - let stderr = ""; - const flushOutputCallbacks = async () => { - let previousSnapshot = ""; - for (let attempt = 0; attempt < 3; attempt++) { - const snapshot = `${stdout.length}:${stderr.length}`; - if (snapshot === previousSnapshot) { - return; - } - previousSnapshot = snapshot; - await new Promise((resolve) => setTimeout(resolve, 0)); - } - }; - - return Promise.race([ - (async () => { - const proc = shell.kernel.spawn(command, args, { - cwd: shell.workDir, - env: shell.env, - onStdout: (chunk) => { - stdout += Buffer.from(chunk).toString("utf8"); - }, - onStderr: (chunk) => { - stderr += Buffer.from(chunk).toString("utf8"); - }, - }); - const exitCode = await proc.wait(); - await flushOutputCallbacks(); - return { exitCode, stdout, stderr }; - })(), - new Promise((_, reject) => - setTimeout( - () => - reject(new Error(`Timed out running: ${command} ${args.join(" ")}`)), - timeoutMs, - ), - ), - ]); -} - -describe("dev-shell integration", { timeout: 60_000 }, () => { - let shell: Awaited> | undefined; - let workDir: string | undefined; - let hostOnlyDir: string | undefined; - - afterEach(async () => { - await shell?.dispose(); - shell = undefined; - if (hostOnlyDir) { - await rm(hostOnlyDir, { recursive: true, force: true }); - hostOnlyDir = undefined; - } - if (workDir) { - await rm(workDir, { recursive: true, force: true }); - workDir = undefined; - } - }); - - it("boots the sandbox-native dev-shell surface and runs node, pi, and the Wasm shell", async () => { - workDir = await mkdtemp(path.join(tmpdir(), "agentos-dev-shell-")); - await writeFile(path.join(workDir, "note.txt"), "dev-shell\n"); - - shell = await createDevShellKernel({ workDir }); - - expect(shell.loadedCommands).toEqual( - expect.arrayContaining(["bash", "node", "npm", "npx", "pi", "sh"]), - ); - expect(shell.loadedCommands).not.toEqual( - expect.arrayContaining(["python", "python3", "pip"]), - ); - - const nodeResult = await runKernelCommand(shell, "node", [ - "-e", - "console.log(process.version)", - ]); - expect(nodeResult.exitCode).toBe(0); - expect(nodeResult.stdout).toMatch(/v\d+\.\d+\.\d+/); - - const shellResult = await runKernelCommand(shell, "bash", [ - "-ic", - "echo shell-ok", - ]); - expect(shellResult.exitCode).toBe(0); - expect(shellResult.stdout).toContain("shell-ok"); - - const piResult = await runKernelCommand(shell, "pi", ["--help"], 30_000); - expect(piResult.exitCode).toBe(0); - expect(`${piResult.stdout}\n${piResult.stderr}`).toMatch(/pi|usage|Usage/); - }); - - it("resolves file listings through the Wasm shell", async () => { - workDir = await mkdtemp(path.join(tmpdir(), "agentos-dev-shell-pty-")); - await writeFile(path.join(workDir, "note.txt"), "pty-dev-shell\n"); - shell = await createDevShellKernel({ workDir }); - - const shellResult = await runKernelCommand(shell, "bash", [ - "-ic", - "ls /bin", - ]); - - expect(shellResult.exitCode).toBe(0); - expect(shellResult.stdout).toContain("npm"); - expect(shellResult.stdout).toContain("npx"); - }); - - it("does not read or execute host-only paths outside the mounted VM roots", async () => { - workDir = await mkdtemp( - path.join(tmpdir(), "agentos-dev-shell-isolated-"), - ); - hostOnlyDir = await mkdtemp("/var/tmp/agentos-dev-shell-host-only-"); - const hostOnlyFile = path.join(hostOnlyDir, "secret.txt"); - const hostOnlyCommand = path.join(hostOnlyDir, "host-only-command.sh"); - - await writeFile(hostOnlyFile, "host-only secret\n"); - await writeFile( - hostOnlyCommand, - "#!/bin/sh\nprintf 'host-only command should stay hidden\\n'\n", - ); - await chmod(hostOnlyCommand, 0o755); - - shell = await createDevShellKernel({ workDir }); - - const readResult = await runKernelCommand(shell, "cat", [hostOnlyFile]); - expect(readResult.exitCode).not.toBe(0); - expect(`${readResult.stdout}\n${readResult.stderr}`).not.toContain( - "host-only secret", - ); - - const execResult = await runKernelCommand(shell, hostOnlyCommand, []); - expect(execResult.exitCode).not.toBe(0); - expect(`${execResult.stdout}\n${execResult.stderr}`).not.toContain( - "host-only command should stay hidden", - ); - }); - - it("keeps dev-shell writes in the VM shadow root instead of mutating the host work dir", async () => { - workDir = await mkdtemp(path.join(tmpdir(), "agentos-dev-shell-shadow-")); - const guestFilePath = path.join(workDir, "note.txt"); - await writeFile(guestFilePath, "host-note\n"); - - shell = await createDevShellKernel({ workDir }); - await shell.kernel.writeFile(guestFilePath, "vm-note\n"); - - const guestReadback = new TextDecoder().decode( - await shell.kernel.readFile(guestFilePath), - ); - expect(guestReadback).toBe("vm-note\n"); - await expect(readFile(guestFilePath, "utf8")).resolves.toBe("host-note\n"); - - const catResult = await runKernelCommand(shell, "cat", [guestFilePath]); - expect(catResult.exitCode).toBe(0); - expect(catResult.stdout).toContain("vm-note"); - }); - - it("mounts /tmp on isolated per-session host temp dirs and removes them on dispose", async () => { - const workDirA = await mkdtemp( - path.join(tmpdir(), "agentos-dev-shell-a-"), - ); - const workDirB = await mkdtemp( - path.join(tmpdir(), "agentos-dev-shell-b-"), - ); - const tempRootsBefore = await listDevShellTempRoots(); - let shellA: Awaited> | undefined; - let shellB: Awaited> | undefined; - let sessionARoot: string | undefined; - let sessionBRoot: string | undefined; - - try { - shellA = await createDevShellKernel({ workDir: workDirA }); - shellB = await createDevShellKernel({ workDir: workDirB }); - - await shellA.kernel.writeFile("/tmp/session-a.txt", "session-a\n"); - await shellB.kernel.writeFile("/tmp/session-b.txt", "session-b\n"); - - await expect(shellA.kernel.exists("/tmp/session-b.txt")).resolves.toBe( - false, - ); - await expect(shellB.kernel.exists("/tmp/session-a.txt")).resolves.toBe( - false, - ); - - const createdRoots = (await listDevShellTempRoots()).filter( - (root) => !tempRootsBefore.includes(root), - ); - expect(createdRoots).toHaveLength(2); - - for (const root of createdRoots) { - expect(path.basename(root)).toMatch( - new RegExp(`^${DEV_SHELL_TMP_ROOT_PREFIX}`), - ); - expect(existsSync(path.join(root, "tmp"))).toBe(true); - } - - const tempRootContents = await Promise.all( - createdRoots.map(async (root) => ({ - root, - entries: await readdir(path.join(root, "tmp")), - })), - ); - sessionARoot = tempRootContents.find((root) => - root.entries.includes("session-a.txt"), - )?.root; - sessionBRoot = tempRootContents.find((root) => - root.entries.includes("session-b.txt"), - )?.root; - expect(sessionARoot).toBeDefined(); - expect(sessionBRoot).toBeDefined(); - expect(sessionARoot).not.toBe(sessionBRoot); - } finally { - await shellA?.dispose(); - await shellB?.dispose(); - await rm(workDirA, { recursive: true, force: true }); - await rm(workDirB, { recursive: true, force: true }); - } - - expect(sessionARoot && existsSync(sessionARoot)).toBe(false); - expect(sessionBRoot && existsSync(sessionBRoot)).toBe(false); - }); -}); - -describe("dev-shell debug logger", { timeout: 60_000 }, () => { - let shell: Awaited> | undefined; - let workDir: string | undefined; - let logDir: string | undefined; - - afterEach(async () => { - await shell?.dispose(); - shell = undefined; - if (workDir) { - await rm(workDir, { recursive: true, force: true }); - workDir = undefined; - } - if (logDir) { - await rm(logDir, { recursive: true, force: true }); - logDir = undefined; - } - }); - - it("writes structured debug logs to the requested file and keeps stdout/stderr clean", async () => { - workDir = await mkdtemp(path.join(tmpdir(), "agentos-debug-log-")); - logDir = await mkdtemp(path.join(tmpdir(), "agentos-debug-log-out-")); - const logPath = path.join(logDir, "debug.ndjson"); - - // Capture process stdout/stderr to detect any contamination. - const origStdoutWrite = process.stdout.write.bind( - process.stdout, - ) as StreamWrite; - const origStderrWrite = process.stderr.write.bind( - process.stderr, - ) as StreamWrite; - const stdoutCapture: string[] = []; - const stderrCapture: string[] = []; - process.stdout.write = ((chunk: unknown, ...rest: unknown[]) => { - if (typeof chunk === "string") stdoutCapture.push(chunk); - else if (Buffer.isBuffer(chunk)) - stdoutCapture.push(chunk.toString("utf8")); - return origStdoutWrite(chunk, ...rest); - }) as typeof process.stdout.write; - process.stderr.write = ((chunk: unknown, ...rest: unknown[]) => { - if (typeof chunk === "string") stderrCapture.push(chunk); - else if (Buffer.isBuffer(chunk)) - stderrCapture.push(chunk.toString("utf8")); - return origStderrWrite(chunk, ...rest); - }) as typeof process.stderr.write; - - try { - shell = await createDevShellKernel({ - workDir, - mountWasm: false, - debugLogPath: logPath, - }); - - // Run a quick command to exercise the kernel. - const proc = shell.kernel.spawn( - "node", - ["-e", "console.log('debug-log-test')"], - { - cwd: shell.workDir, - env: shell.env, - }, - ); - await proc.wait(); - - await shell.dispose(); - shell = undefined; - } finally { - process.stdout.write = origStdoutWrite; - process.stderr.write = origStderrWrite; - } - - // The log file must exist and contain structured JSON lines. - expect(existsSync(logPath)).toBe(true); - const logContent = await readFile(logPath, "utf8"); - const lines = logContent.trim().split("\n").filter(Boolean); - expect(lines.length).toBeGreaterThanOrEqual(1); - - // Every line must be valid JSON with a timestamp. - for (const line of lines) { - const record = JSON.parse(line); - expect(record).toHaveProperty("time"); - } - - // At least one record should reference session init. - const initRecord = lines.find((line) => - line.includes("dev-shell session init"), - ); - expect(initRecord).toBeDefined(); - - // Stdout/stderr must not contain any pino JSON records. - const combinedOutput = [...stdoutCapture, ...stderrCapture].join(""); - for (const line of lines) { - expect(combinedOutput).not.toContain(line); - } - }); - - it("emits kernel diagnostic records for spawn, process exit, and PTY operations", async () => { - workDir = await mkdtemp(path.join(tmpdir(), "agentos-debug-diag-")); - logDir = await mkdtemp(path.join(tmpdir(), "agentos-debug-diag-out-")); - const logPath = path.join(logDir, "debug.ndjson"); - - shell = await createDevShellKernel({ - workDir, - mountWasm: false, - debugLogPath: logPath, - }); - - // Spawn a command to exercise kernel spawn/exit logging - const proc = shell.kernel.spawn( - "node", - ["-e", "console.log('diag-test')"], - { - cwd: shell.workDir, - env: shell.env, - }, - ); - await proc.wait(); - - await shell.dispose(); - shell = undefined; - - const logContent = await readFile(logPath, "utf8"); - const lines = logContent.trim().split("\n").filter(Boolean); - const records = lines.map((l) => JSON.parse(l)); - - // Must contain spawn and exit diagnostics from the kernel - const spawnRecord = records.find( - (r: Record) => - r.msg === "process spawned" && - (r as Record).command === "node", - ); - expect(spawnRecord).toBeDefined(); - expect(spawnRecord).toHaveProperty("pid"); - expect(spawnRecord).toHaveProperty("driver"); - - const exitRecord = records.find( - (r: Record) => - r.msg === "process exited" && - (r as Record).command === "node", - ); - expect(exitRecord).toBeDefined(); - expect(exitRecord).toHaveProperty("exitCode", 0); - - // Must contain driver mount diagnostics - const mountRecord = records.find( - (r: Record) => r.msg === "runtime driver mounted", - ); - expect(mountRecord).toBeDefined(); - - // Every record must have a timestamp - for (const record of records) { - expect(record).toHaveProperty("time"); - } - }); - - it("redacts secret keys in log records", async () => { - workDir = await mkdtemp(path.join(tmpdir(), "agentos-debug-log-redact-")); - logDir = await mkdtemp( - path.join(tmpdir(), "agentos-debug-log-redact-out-"), - ); - const logPath = path.join(logDir, "debug.ndjson"); - - shell = await createDevShellKernel({ - workDir, - mountWasm: false, - debugLogPath: logPath, - }); - - // Log a record that includes a sensitive key. - shell.logger.info( - { - env: { ANTHROPIC_API_KEY: "sk-ant-secret-value", SAFE_VAR: "visible" }, - }, - "env snapshot", - ); - - await shell.dispose(); - shell = undefined; - - const logContent = await readFile(logPath, "utf8"); - expect(logContent).not.toContain("sk-ant-secret-value"); - expect(logContent).toContain("[REDACTED]"); - expect(logContent).toContain("visible"); - }); -}); diff --git a/packages/dev-shell/test/terminal-harness.ts b/packages/dev-shell/test/terminal-harness.ts deleted file mode 100644 index 745e8815e..000000000 --- a/packages/dev-shell/test/terminal-harness.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type { Kernel } from "@rivet-dev/agentos-core/test/runtime"; -import { Terminal } from "@xterm/headless"; - -type ShellHandle = ReturnType; - -const SETTLE_MS = 50; -const POLL_MS = 20; -const DEFAULT_WAIT_TIMEOUT_MS = 5_000; - -export class TerminalHarness { - readonly term: Terminal; - readonly shell: ShellHandle; - private typing = false; - private disposed = false; - - constructor( - kernel: Kernel, - options?: { - cols?: number; - rows?: number; - env?: Record; - cwd?: string; - }, - ) { - const cols = options?.cols ?? 80; - const rows = options?.rows ?? 24; - - this.term = new Terminal({ cols, rows, allowProposedApi: true }); - this.shell = kernel.openShell({ - cols, - rows, - env: options?.env, - cwd: options?.cwd, - }); - this.shell.onData = (data: Uint8Array) => { - this.term.write(data); - }; - } - - async type(input: string): Promise { - if (this.typing) { - throw new Error( - "TerminalHarness.type() called while previous type() is still in-flight", - ); - } - this.typing = true; - try { - await this.typeInternal(input); - } finally { - this.typing = false; - } - } - - private typeInternal(input: string): Promise { - return new Promise((resolve) => { - let timer: ReturnType | null = null; - - const originalOnData = this.shell.onData; - const resetTimer = () => { - if (timer !== null) clearTimeout(timer); - timer = setTimeout(() => { - this.shell.onData = originalOnData; - resolve(); - }, SETTLE_MS); - }; - - this.shell.onData = (data: Uint8Array) => { - this.term.write(data); - resetTimer(); - }; - - resetTimer(); - this.shell.write(input); - }); - } - - screenshotTrimmed(): string { - const buf = this.term.buffer.active; - const lines: string[] = []; - - for (let row = 0; row < this.term.rows; row++) { - const line = buf.getLine(buf.viewportY + row); - lines.push(line ? line.translateToString(true) : ""); - } - - while (lines.length > 0 && lines[lines.length - 1] === "") { - lines.pop(); - } - - return lines.join("\n"); - } - - async waitFor( - text: string, - occurrence = 1, - timeoutMs = DEFAULT_WAIT_TIMEOUT_MS, - ): Promise { - const deadline = Date.now() + timeoutMs; - - while (true) { - const screen = this.screenshotTrimmed(); - let count = 0; - let idx = -1; - - while (true) { - idx = screen.indexOf(text, idx + 1); - if (idx === -1) break; - count++; - if (count >= occurrence) return; - } - - if (Date.now() >= deadline) { - throw new Error( - `waitFor("${text}", ${occurrence}) timed out after ${timeoutMs}ms.\n` + - `Expected: "${text}" (occurrence ${occurrence})\n` + - `Screen:\n${screen}`, - ); - } - - await new Promise((resolve) => setTimeout(resolve, POLL_MS)); - } - } - - async dispose(): Promise { - if (this.disposed) return; - this.disposed = true; - - try { - this.shell.kill(); - await Promise.race([ - this.shell.wait(), - new Promise((resolve) => setTimeout(resolve, 500)), - ]); - } catch { - // Shell may already be gone. - } - - this.term.dispose(); - } -} diff --git a/packages/dev-shell/tsconfig.json b/packages/dev-shell/tsconfig.json deleted file mode 100644 index 4f8cbee62..000000000 --- a/packages/dev-shell/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/dev-shell/vitest.config.ts b/packages/dev-shell/vitest.config.ts deleted file mode 100644 index 0d0a4e3b9..000000000 --- a/packages/dev-shell/vitest.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - // The dev-shell suite spins up full Wasm/Node runtimes and the justfile wrapper. - // Running files concurrently can produce intermittent crashes under workspace load. - fileParallelism: false, - include: ["test/**/*.test.ts"], - testTimeout: 60000, - }, -}); diff --git a/packages/posix/package.json b/packages/posix/package.json index 58fedee3b..c5a2f1eb0 100644 --- a/packages/posix/package.json +++ b/packages/posix/package.json @@ -23,7 +23,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@secure-exec/core": "catalog:" + "@secure-exec/core": "link:../../../secure-exec/packages/core" }, "devDependencies": { "@types/node": "^22.10.2", diff --git a/packages/python/package.json b/packages/python/package.json index 49af7b622..5e9306a9e 100644 --- a/packages/python/package.json +++ b/packages/python/package.json @@ -32,7 +32,7 @@ "test": "pnpm build && vitest run --fileParallelism=false" }, "dependencies": { - "@secure-exec/core": "catalog:", + "@secure-exec/core": "link:../../../secure-exec/packages/core", "pyodide": "^0.28.3" }, "peerDependencies": { diff --git a/packages/shell/CLAUDE.md b/packages/shell/CLAUDE.md new file mode 100644 index 000000000..560fdcd30 --- /dev/null +++ b/packages/shell/CLAUDE.md @@ -0,0 +1,4 @@ +# agentos-shell + +- 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. diff --git a/packages/shell/package.json b/packages/shell/package.json index e63dc52b9..1f8158a7c 100644 --- a/packages/shell/package.json +++ b/packages/shell/package.json @@ -1,9 +1,11 @@ { "name": "@rivet-dev/agentos-shell", - "private": true, + "version": "0.2.0-rc.3", + "private": false, "type": "module", "bin": { - "agentos-shell": "./dist/main.js" + "agentos-shell": "./dist/main.js", + "agent-os-shell": "./dist/main.js" }, "scripts": { "build": "tsc", @@ -13,16 +15,31 @@ }, "dependencies": { "@rivet-dev/agentos-core": "workspace:*", - "@agentos-software/common": "catalog:", - "@agentos-software/jq": "catalog:", - "@agentos-software/ripgrep": "catalog:", - "@agentos-software/fd": "catalog:", - "@agentos-software/tree": "catalog:", - "@agentos-software/file": "catalog:", - "@agentos-software/zip": "catalog:", - "@agentos-software/unzip": "catalog:", - "@agentos-software/yq": "catalog:", - "@agentos-software/codex-cli": "catalog:" + "@agentos-software/codex-cli": "link:../../../secure-exec/registry/software/codex", + "@agentos-software/coreutils": "link:../../../secure-exec/registry/software/coreutils", + "@agentos-software/curl": "link:../../../secure-exec/registry/software/curl", + "@agentos-software/diffutils": "link:../../../secure-exec/registry/software/diffutils", + "@agentos-software/duckdb": "link:../../../secure-exec/registry/software/duckdb", + "@agentos-software/fd": "link:../../../secure-exec/registry/software/fd", + "@agentos-software/file": "link:../../../secure-exec/registry/software/file", + "@agentos-software/findutils": "link:../../../secure-exec/registry/software/findutils", + "@agentos-software/gawk": "link:../../../secure-exec/registry/software/gawk", + "@agentos-software/git": "link:../../../secure-exec/registry/software/git", + "@agentos-software/grep": "link:../../../secure-exec/registry/software/grep", + "@agentos-software/gzip": "link:../../../secure-exec/registry/software/gzip", + "@agentos-software/http-get": "link:../../../secure-exec/registry/software/http-get", + "@agentos-software/jq": "link:../../../secure-exec/registry/software/jq", + "@agentos-software/make": "link:../../../secure-exec/registry/software/make", + "@agentos-software/ripgrep": "link:../../../secure-exec/registry/software/ripgrep", + "@agentos-software/sed": "link:../../../secure-exec/registry/software/sed", + "@agentos-software/sqlite3": "link:../../../secure-exec/registry/software/sqlite3", + "@agentos-software/tar": "link:../../../secure-exec/registry/software/tar", + "@agentos-software/tree": "link:../../../secure-exec/registry/software/tree", + "@agentos-software/unzip": "link:../../../secure-exec/registry/software/unzip", + "@agentos-software/wget": "link:../../../secure-exec/registry/software/wget", + "@agentos-software/yq": "link:../../../secure-exec/registry/software/yq", + "@agentos-software/zip": "link:../../../secure-exec/registry/software/zip", + "commander": "^14.0.2" }, "devDependencies": { "@types/node": "^22.19.3", diff --git a/packages/shell/src/main.ts b/packages/shell/src/main.ts index 8b6a25e8a..ef5036110 100644 --- a/packages/shell/src/main.ts +++ b/packages/shell/src/main.ts @@ -1,45 +1,109 @@ #!/usr/bin/env node -import { existsSync } from "node:fs"; -import { dirname, resolve } from "node:path"; +/** + * 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, + * `-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 + * Agent OS's terminal API instead of a custom prompt or line editor; non-TTY + * commands use process spawn with Docker-like stdin attachment rules. + */ + +import { existsSync, readFileSync } from "node:fs"; +import { basename, dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import codex from "@agentos-software/codex-cli"; -import common from "@agentos-software/common"; +import coreutils from "@agentos-software/coreutils"; +import curl from "@agentos-software/curl"; +import diffutils from "@agentos-software/diffutils"; +import duckdb from "@agentos-software/duckdb"; import fd from "@agentos-software/fd"; import file from "@agentos-software/file"; +import findutils from "@agentos-software/findutils"; +import gawk from "@agentos-software/gawk"; +import git from "@agentos-software/git"; +import grep from "@agentos-software/grep"; +import gzip from "@agentos-software/gzip"; +import httpGet from "@agentos-software/http-get"; import jq from "@agentos-software/jq"; +import make from "@agentos-software/make"; import ripgrep from "@agentos-software/ripgrep"; +import sed from "@agentos-software/sed"; +import sqlite3 from "@agentos-software/sqlite3"; +import tar from "@agentos-software/tar"; import tree from "@agentos-software/tree"; import unzip from "@agentos-software/unzip"; +import wget from "@agentos-software/wget"; import yq from "@agentos-software/yq"; import zip from "@agentos-software/zip"; -import type { SoftwareInput } from "@rivet-dev/agentos-core"; import { AgentOs } from "@rivet-dev/agentos-core"; +import type { MountConfig, SoftwareInput } from "@rivet-dev/agentos-core"; +import { allowAll } from "@rivet-dev/agentos-core/internal/runtime-compat"; +import { Command, Option } from "commander"; const __dirname = dirname(fileURLToPath(import.meta.url)); -const COMMAND_SUBPATH = "registry/native/target/wasm32-wasip1/release/commands"; +const workspaceRoot = resolve(__dirname, "../../.."); +const fallbackCommandDirs = [ + resolve( + workspaceRoot, + "registry/native/target/wasm32-wasip1/release/commands", + ), + resolve( + workspaceRoot, + "../secure-exec/registry/native/target/wasm32-wasip1/release/commands", + ), +]; +const INTERACTIVE_SHELL_WASM_FUEL_MS = 24 * 60 * 60 * 1000; +const BRUSH_SHELL_COMMANDS = new Set(["bash", "sh"]); +const SHELL_OPTIONS_WITH_VALUES = new Set([ + "--command", + "--debuglog-enable", + "--init-file", + "--input-backend", + "--log-disable", + "--rcfile", + "-c", +]); + +interface CliOptions { + interactive: boolean; + tty: boolean; + workdir: string; + env: string[]; + envFile: string[]; + volume: string[]; + mount: string[]; + rm: boolean; + name?: string; + command: string; + args: string[]; +} // Published packages ship package-local wasm/ dirs. Workspace packages use the -// sibling secure-exec native build output. -const fallbackCommandDir = [ - resolve(__dirname, "../../../../secure-exec", COMMAND_SUBPATH), -].find((dir) => existsSync(dir)); +// native build output when those package-local dirs have not been materialized. function withLocalCommandFallback(software: SoftwareInput): SoftwareInput { if (Array.isArray(software)) { return software.map(withLocalCommandFallback) as SoftwareInput; } if ( - fallbackCommandDir !== undefined && "commandDir" in software && typeof software.commandDir === "string" && !existsSync(software.commandDir) ) { - const dir = fallbackCommandDir; + const fallbackCommandDir = fallbackCommandDirs.find((dir) => + existsSync(dir), + ); + if (!fallbackCommandDir) { + return software; + } return { ...software, get commandDir() { - return dir; + return fallbackCommandDir; }, }; } @@ -48,139 +112,479 @@ function withLocalCommandFallback(software: SoftwareInput): SoftwareInput { } const software = [ - common, + coreutils, + sed, + grep, + gawk, + findutils, + diffutils, + tar, + gzip, + curl, + zip, + unzip, jq, ripgrep, fd, tree, file, - zip, - unzip, yq, codex, + git, + make, + duckdb, + httpGet, + sqlite3, + wget, ].map(withLocalCommandFallback); -function printUsage(): void { - console.error( - [ - "Usage:", - " agentos-shell [--work-dir ] [--] [command] [args...]", - "", - "Options:", - " --work-dir Set the working directory inside the VM (default: /home/agentos)", - " --help, -h Show this help", - "", - "Examples:", - " pnpm shell", - " pnpm shell --work-dir /tmp/demo", - " pnpm shell -- node -e 'console.log(42)'", - ].join("\n"), - ); +function createShellDiagnosticStripper(): (data: Uint8Array) => Uint8Array | null { + let suppressUntilNewline = false; + return (data: Uint8Array) => { + let text = Buffer.from(data).toString("utf8"); + let output = ""; + + while (text.length > 0) { + if (suppressUntilNewline) { + const newlineIndex = text.indexOf("\n"); + if (newlineIndex < 0) { + return output.length > 0 ? Buffer.from(output, "utf8") : null; + } + text = text.slice(newlineIndex + 1); + suppressUntilNewline = false; + continue; + } + + const warningIndex = text.indexOf("WARN could not retrieve pid"); + if (warningIndex < 0) { + output += text; + break; + } + + const lineStartIndex = text.lastIndexOf("\n", warningIndex); + const lineStart = lineStartIndex < 0 ? 0 : lineStartIndex + 1; + output += text.slice(0, lineStart); + + const lineEnd = text.indexOf("\n", warningIndex); + if (lineEnd < 0) { + suppressUntilNewline = true; + break; + } + text = text.slice(lineEnd + 1); + } + + return output.length > 0 ? Buffer.from(output, "utf8") : null; + }; } -interface CliOptions { - workDir?: string; - command: string; - args: string[]; +function collectOption(value: string, previous: string[]): string[] { + previous.push(value); + return previous; } -function parseArgs(argv: string[]): CliOptions { - const options: CliOptions = { - command: "bash", - args: [], +function parseCli(argv: string[]): CliOptions { + const program = new Command() + .name("agentos-shell") + .description("Run a command or terminal inside an Agent OS VM.") + .exitOverride() + .passThroughOptions() + .allowExcessArguments() + .argument("[command]", "guest command to run", "bash") + .argument("[args...]", "guest command arguments") + .addOption( + new Option("-i, --interactive", "keep stdin attached") + .default(false), + ) + .addOption(new Option("-t, --tty", "connect a terminal").default(false)) + .option( + "-e, --env ", + "set environment variable (KEY=VALUE or KEY to copy from host)", + collectOption, + [], + ) + .option( + "--env-file ", + "read environment variables from a file", + collectOption, + [], + ) + .option( + "-v, --volume ", + "bind mount a volume (host:guest[:ro|rw])", + collectOption, + [], + ) + .option( + "--mount ", + "bind mount using Docker syntax (type=bind,src=...,target=...,readonly)", + collectOption, + [], + ) + .option("-w, --workdir ", "working directory inside the VM", "/") + .option("--name ", "container-style name label (accepted for Docker CLI parity)") + .option("--rm", "remove VM after exit (always true for this CLI)", false); + + try { + program.parse(["node", "agentos-shell", ...argv]); + } catch (error) { + if ( + error && + typeof error === "object" && + "code" in error && + error.code === "commander.helpDisplayed" + ) { + process.exit(0); + } + throw error; + } + + const opts = program.opts<{ + interactive: boolean; + tty: boolean; + workdir: string; + env: string[]; + envFile: string[]; + volume: string[]; + mount: string[]; + rm: boolean; + name?: string; + }>(); + const [command = "bash", ...args] = program.args; + + return { + interactive: opts.interactive, + tty: opts.tty, + workdir: opts.workdir, + env: opts.env, + envFile: opts.envFile, + volume: opts.volume, + mount: opts.mount, + rm: opts.rm, + name: opts.name, + command, + args, }; +} - for (let i = 0; i < argv.length; i++) { - const arg = argv[i]; - if (arg === "--") { - const trailing = argv.slice(i + 1); - if (trailing.length > 0) { - options.command = trailing[0]; - options.args = trailing.slice(1); +function parseEnvLine(line: string): [string, string] | null { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) { + return null; + } + const equalsIndex = trimmed.indexOf("="); + if (equalsIndex < 0) { + const hostValue = process.env[trimmed]; + return hostValue === undefined ? null : [trimmed, hostValue]; + } + return [trimmed.slice(0, equalsIndex), trimmed.slice(equalsIndex + 1)]; +} + +function buildEnv(options: CliOptions): Record { + const env: Record = {}; + for (const envFilePath of options.envFile) { + const content = readFileSync(resolve(envFilePath), "utf8"); + for (const line of content.split(/\r?\n/)) { + const entry = parseEnvLine(line); + if (entry) { + env[entry[0]] = entry[1]; } - break; } + } + for (const value of options.env) { + const entry = parseEnvLine(value); + if (entry) { + env[entry[0]] = entry[1]; + } + } + return env; +} - if (!arg.startsWith("-")) { - options.command = arg; - options.args = argv.slice(i + 1); - break; +function hostDirMount( + hostPath: string, + guestPath: string, + readOnly: boolean, +): MountConfig { + return { + path: guestPath, + readOnly, + plugin: { + id: "host_dir", + config: { + hostPath: resolve(hostPath), + readOnly, + }, + }, + }; +} + +function parseVolumeSpec(spec: string): MountConfig { + const [hostPath, guestPath, mode] = spec.split(":"); + if (!hostPath || !guestPath) { + throw new Error(`Invalid volume spec "${spec}"; expected host:guest[:ro|rw]`); + } + if (mode && mode !== "ro" && mode !== "rw") { + throw new Error(`Invalid volume mode "${mode}" in "${spec}"`); + } + return hostDirMount(hostPath, guestPath, mode === "ro"); +} + +function parseMountSpec(spec: string): MountConfig { + const fields = new Map(); + for (const rawPart of spec.split(",")) { + const part = rawPart.trim(); + if (!part) { + continue; } + const equalsIndex = part.indexOf("="); + if (equalsIndex < 0) { + fields.set(part, true); + } else { + fields.set(part.slice(0, equalsIndex), part.slice(equalsIndex + 1)); + } + } - switch (arg) { - case "--work-dir": - if (!argv[i + 1]) { - throw new Error("--work-dir requires a path"); - } - options.workDir = argv[++i]; - break; - case "--help": - case "-h": - printUsage(); - process.exit(0); - return options; - default: - throw new Error(`Unknown argument: ${arg}`); + if (fields.get("type") !== "bind") { + throw new Error(`Only bind mounts are supported: --mount ${spec}`); + } + const source = fields.get("source") ?? fields.get("src"); + const target = fields.get("target") ?? fields.get("dst") ?? fields.get("destination"); + if (typeof source !== "string" || typeof target !== "string") { + throw new Error( + `Invalid mount spec "${spec}"; expected type=bind,source=...,target=...`, + ); + } + const readOnly = fields.has("readonly") || fields.get("ro") === "true"; + return hostDirMount(source, target, readOnly); +} + +function buildMounts(options: CliOptions): MountConfig[] { + return [ + ...options.volume.map(parseVolumeSpec), + ...options.mount.map(parseMountSpec), + ]; +} + +function isBrushShellCommand(command: string): boolean { + return BRUSH_SHELL_COMMANDS.has(basename(command)); +} + +function hasBrushInputBackend(args: string[]): boolean { + return args.some( + (arg) => arg === "--input-backend" || arg.startsWith("--input-backend="), + ); +} + +function hasInteractiveShellFlag(args: string[]): boolean { + return args.some((arg) => arg === "-i" || arg === "--interactive"); +} + +function shellArgsRequestCommandOrScript(args: string[]): boolean { + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === "--") { + return i + 1 < args.length; + } + if (arg.startsWith("--") && arg.includes("=")) { + if (arg.startsWith("--command=")) { + return true; + } + continue; } + if (SHELL_OPTIONS_WITH_VALUES.has(arg)) { + if (arg === "-c" || arg === "--command") { + return true; + } + i++; + continue; + } + if (arg.startsWith("-")) { + continue; + } + return true; } + return false; +} - return options; +function buildTerminalCommand(options: CliOptions): { + command: string; + args: string[]; +} { + const args = [...options.args]; + if (isBrushShellCommand(options.command)) { + if (!hasBrushInputBackend(args)) { + args.unshift("--input-backend", "reedline"); + } + if ( + !hasInteractiveShellFlag(args) && + !shellArgsRequestCommandOrScript(args) + ) { + args.push("-i"); + } + } + return { + command: options.command, + args, + }; } -async function runCommand( +async function runSpawnedCommand( vm: AgentOs, - cli: CliOptions, - cwd: string, + options: CliOptions, + env: Record, ): Promise { - const args = - (cli.command === "bash" || cli.command === "sh") && cli.args.length === 0 - ? ["-i"] - : cli.args; - const child = vm.spawn(cli.command, args, { - cwd, + const child = vm.spawn(options.command, options.args, { + cwd: options.workdir, + env, + streamStdin: options.interactive, onStdout: (data) => { process.stdout.write(data); }, - onStderr: (data) => { + onStderr: (data: Uint8Array) => { process.stderr.write(data); }, }); - const restoreRawMode = - process.stdin.isTTY && typeof process.stdin.setRawMode === "function"; + let stdinQueue = Promise.resolve(); + const queueStdin = (operation: () => Promise) => { + stdinQueue = stdinQueue.then(operation); + void stdinQueue.catch((error) => { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`${message}\n`); + }); + }; + const closeChildStdin = () => { + queueStdin(async () => { + try { + await vm.closeProcessStdin(child.pid); + } catch { + // The process may have already exited before host stdin reports EOF. + } + }); + }; const onStdinData = (data: Uint8Array | string) => { - vm.writeProcessStdin(child.pid, data); + queueStdin(() => vm.writeProcessStdin(child.pid, data)); }; + if (!options.interactive) { + closeChildStdin(); + return vm.waitProcess(child.pid); + } + try { - if (restoreRawMode) { - process.stdin.setRawMode(true); - } process.stdin.on("data", onStdinData); + process.stdin.once("end", closeChildStdin); + process.stdin.once("error", closeChildStdin); process.stdin.resume(); return await vm.waitProcess(child.pid); } finally { process.stdin.removeListener("data", onStdinData); + process.stdin.removeListener("end", closeChildStdin); + process.stdin.removeListener("error", closeChildStdin); process.stdin.pause(); - if (restoreRawMode) { + } +} + +async function runTerminalCommand( + vm: AgentOs, + options: CliOptions, + env: Record, +): Promise { + const stripDiagnostics = createShellDiagnosticStripper(); + const shellOptions = { + cwd: options.workdir, + env, + cols: process.stdout.columns, + rows: process.stdout.rows, + onStderr: (data: Uint8Array) => { + const sanitized = stripDiagnostics(data); + if (sanitized) process.stderr.write(sanitized); + }, + }; + const { shellId } = vm.openShell({ + ...shellOptions, + ...buildTerminalCommand(options), + }); + let stdinQueue = Promise.resolve(); + const queueShellInput = (data: Uint8Array | string) => { + stdinQueue = stdinQueue.then(() => vm.writeShell(shellId, data)); + void stdinQueue.catch((error) => { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`${message}\n`); + }); + }; + const onStdinData = (data: Uint8Array | string) => { + queueShellInput(data); + }; + const onStdinEnd = () => { + queueShellInput("\u0004"); + }; + const onResize = () => { + vm.resizeShell(shellId, process.stdout.columns, process.stdout.rows); + }; + const unsubscribeOutput = vm.onShellData(shellId, (data) => { + const sanitized = stripDiagnostics(data); + if (sanitized) process.stdout.write(sanitized); + }); + const canUseRawMode = + options.interactive && + process.stdin.isTTY && + typeof process.stdin.setRawMode === "function"; + let rawModeEnabled = false; + + try { + if (options.interactive) { + if (canUseRawMode) { + process.stdin.setRawMode(true); + rawModeEnabled = true; + } + process.stdin.on("data", onStdinData); + process.stdin.once("end", onStdinEnd); + process.stdin.once("error", onStdinEnd); + process.stdin.resume(); + } + if (process.stdout.isTTY) { + process.stdout.on("resize", onResize); + onResize(); + } + + return await vm.waitShell(shellId); + } finally { + unsubscribeOutput(); + process.stdin.removeListener("data", onStdinData); + process.stdin.removeListener("end", onStdinEnd); + process.stdin.removeListener("error", onStdinEnd); + process.stdin.pause(); + if (rawModeEnabled) { process.stdin.setRawMode(false); } + if (process.stdout.isTTY) { + process.stdout.removeListener("resize", onResize); + } } } -const cli = parseArgs(process.argv.slice(2)); +const cli = parseCli(process.argv.slice(2)); +const env = buildEnv(cli); +const mounts = buildMounts(cli); const vm = await AgentOs.create({ + mounts, + permissions: allowAll, software, + limits: cli.tty + ? { + resources: { + maxWasmFuel: INTERACTIVE_SHELL_WASM_FUEL_MS, + }, + } + : undefined, }); -const cwd = cli.workDir ?? "/home/agentos"; - -console.error("agent-os shell"); -console.error(`cwd: ${cwd}`); - let exitCode = 1; try { - exitCode = await runCommand(vm, cli, cwd); + const useTerminal = cli.tty && process.stdin.isTTY && process.stdout.isTTY; + exitCode = useTerminal + ? await runTerminalCommand(vm, cli, env) + : await runSpawnedCommand(vm, cli, env); } finally { await vm.dispose(); } diff --git a/packages/shell/tests/cli.test.ts b/packages/shell/tests/cli.test.ts index e63dc34ff..a1c576254 100644 --- a/packages/shell/tests/cli.test.ts +++ b/packages/shell/tests/cli.test.ts @@ -1,4 +1,6 @@ import { spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, test } from "vitest"; @@ -6,42 +8,106 @@ import { describe, expect, test } from "vitest"; const packageRoot = dirname(dirname(fileURLToPath(import.meta.url))); const cliPath = join(packageRoot, "dist", "main.js"); +function runCli(args: string[], input?: string) { + return spawnSync(process.execPath, [cliPath, ...args], { + cwd: packageRoot, + encoding: "utf8", + input, + timeout: 60_000, + }); +} + describe("agentos-shell cli", () => { - test("--help prints usage without starting a VM", () => { - const result = spawnSync(process.execPath, [cliPath, "--help"], { - cwd: packageRoot, - encoding: "utf8", - }); + test("--help prints Docker-style run flags without starting a VM", () => { + const result = runCli(["--help"]); expect(result.status).toBe(0); - expect(result.stderr).toContain("Usage:"); - expect(result.stderr).toContain("agentos-shell [--work-dir ]"); + expect(result.stdout).toContain("agentos-shell"); + expect(result.stdout).toContain("-i, --interactive"); + expect(result.stdout).toContain("-t, --tty"); + expect(result.stdout).toContain("-e, --env "); + expect(result.stdout).toContain("-v, --volume "); expect(result.stderr).not.toContain("agent-os shell"); - expect(result.stdout).toBe(""); - }); - - test("runs a VM-backed command and exits with the guest status", () => { - const result = spawnSync( - process.execPath, - [ - cliPath, - "--work-dir", - "/tmp", - "--", - "node", - "-e", - "console.log('SHELL_VM_COMMAND:' + process.cwd()); process.exit(7);", - ], - { - cwd: packageRoot, - encoding: "utf8", - timeout: 60_000, - }, - ); + }); + + test("runs a VM-backed command with guest cwd and env", () => { + const result = runCli([ + "--workdir", + "/tmp", + "--env", + "SHELL_TEST_ENV=works", + "--", + "node", + "-e", + "console.log('SHELL_VM_COMMAND:' + process.cwd() + ':' + process.env.SHELL_TEST_ENV); process.exit(7);", + ]); expect(result.status).toBe(7); - expect(result.stderr).toContain("agent-os shell"); - expect(result.stderr).toContain("cwd: /tmp"); - expect(result.stdout).toContain("SHELL_VM_COMMAND:/tmp"); + expect(result.stdout).toContain("SHELL_VM_COMMAND:/tmp:works"); + }); + + test("mounts a host directory with Docker -v syntax", () => { + const hostDir = mkdtempSync(join(tmpdir(), "agentos-shell-volume-")); + writeFileSync(join(hostDir, "hello.txt"), "mounted\n"); + + const result = runCli([ + "--volume", + `${hostDir}:/mnt:ro`, + "--", + "cat", + "/mnt/hello.txt", + ]); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("mounted"); + }); + + test("keeps stdin attached when -i is set", () => { + const result = runCli(["--interactive", "--", "bash"], "echo hello\n"); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("hello"); + }); + + test("runs a command through terminal mode when -t is set", () => { + const result = runCli([ + "--workdir", + "/tmp", + "--tty", + "--", + "bash", + "-c", + "echo tty-mode", + ]); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("tty-mode"); + }); + + test("default terminal mode launches bash instead of the synthetic shell", () => { + const result = runCli(["--interactive", "--tty"], "echo $0\nexit\n"); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("bash"); + expect(result.stdout).not.toContain("sh-0.4$"); + }); + + test("reads env files", () => { + const dir = mkdtempSync(join(tmpdir(), "agentos-shell-env-")); + mkdirSync(join(dir, "nested")); + const envFile = join(dir, "nested", "env.list"); + writeFileSync(envFile, "FROM_ENV_FILE=yes\n"); + + const result = runCli([ + "--env-file", + envFile, + "--", + "node", + "-e", + "console.log(process.env.FROM_ENV_FILE)", + ]); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("yes"); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0020f8ff3..b384fb64b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,96 +6,9 @@ settings: catalogs: default: - '@agentos-software/claude-code': - specifier: 0.0.0-nathan-binding-workspace.9be0a88 - version: 0.0.0-nathan-binding-workspace.9be0a88 - '@agentos-software/codex': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/codex-cli': - specifier: 0.0.0-codex-claude-runtime-fixes.9cbef3a - version: 0.0.0-codex-claude-runtime-fixes.9cbef3a - '@agentos-software/common': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/coreutils': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/curl': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/diffutils': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/fd': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/file': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/findutils': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/gawk': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/git': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/grep': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/gzip': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/jq': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/opencode': - specifier: 0.0.0-nathan-binding-workspace.9be0a88 - version: 0.0.0-nathan-binding-workspace.9be0a88 - '@agentos-software/pi': - specifier: 0.0.0-nathan-binding-workspace.9be0a88 - version: 0.0.0-nathan-binding-workspace.9be0a88 - '@agentos-software/pi-cli': - specifier: 0.0.0-nathan-binding-workspace.9be0a88 - version: 0.0.0-nathan-binding-workspace.9be0a88 - '@agentos-software/ripgrep': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/sed': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/tar': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/tree': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/unzip': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/yq': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@agentos-software/zip': - specifier: 0.3.0-rc.2 - version: 0.3.0-rc.2 - '@secure-exec/core': - specifier: 0.3.2 - version: 0.3.2 - '@secure-exec/google-drive': - specifier: 0.3.2 - version: 0.3.2 '@secure-exec/nodejs': specifier: 0.2.1 version: 0.2.1 - '@secure-exec/s3': - specifier: 0.3.2 - version: 0.3.2 - '@secure-exec/sandbox': - specifier: 0.3.2 - version: 0.3.2 overrides: '@rivet-dev/agentos-core': workspace:* @@ -108,17 +21,17 @@ importers: .: devDependencies: '@agentos-software/claude-code': - specifier: 'catalog:' - version: 0.0.0-nathan-binding-workspace.9be0a88(@cfworker/json-schema@4.1.1) + specifier: link:../secure-exec/registry/agent/claude + version: link:../secure-exec/registry/agent/claude '@agentos-software/codex': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../secure-exec/registry/agent/codex + version: link:../secure-exec/registry/agent/codex '@agentos-software/common': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../secure-exec/registry/software/common + version: link:../secure-exec/registry/software/common '@agentos-software/pi': - specifier: 'catalog:' - version: 0.0.0-nathan-binding-workspace.9be0a88(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.20.0(bufferutil@4.1.0))(zod@4.3.6) + specifier: link:../secure-exec/registry/agent/pi + version: link:../secure-exec/registry/agent/pi '@biomejs/biome': specifier: ^2.3 version: 2.4.10 @@ -132,8 +45,8 @@ importers: specifier: workspace:* version: link:packages/core '@secure-exec/core': - specifier: 'catalog:' - version: 0.3.2 + specifier: link:../secure-exec/packages/core + version: link:../secure-exec/packages/core '@types/node': specifier: ^22.19.15 version: 22.19.15 @@ -194,17 +107,17 @@ importers: examples/quickstart: dependencies: '@agentos-software/claude-code': - specifier: 'catalog:' - version: 0.0.0-nathan-binding-workspace.9be0a88(@cfworker/json-schema@4.1.1) + specifier: link:../../../secure-exec/registry/agent/claude + version: link:../../../secure-exec/registry/agent/claude '@agentos-software/git': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/git + version: link:../../../secure-exec/registry/software/git '@agentos-software/opencode': - specifier: 'catalog:' - version: 0.0.0-nathan-binding-workspace.9be0a88 + specifier: link:../../../secure-exec/registry/agent/opencode + version: link:../../../secure-exec/registry/agent/opencode '@agentos-software/pi': - specifier: 'catalog:' - version: 0.0.0-nathan-binding-workspace.9be0a88(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.20.0(bufferutil@4.1.0))(zod@4.3.6) + specifier: link:../../../secure-exec/registry/agent/pi + version: link:../../../secure-exec/registry/agent/pi '@rivet-dev/agentos-core': specifier: workspace:* version: link:../../packages/core @@ -212,8 +125,8 @@ importers: specifier: workspace:* version: link:../../packages/agentos-sandbox '@secure-exec/s3': - specifier: 'catalog:' - version: 0.3.2 + specifier: link:../../../secure-exec/registry/file-system/s3 + version: link:../../../secure-exec/registry/file-system/s3 dockerode: specifier: ^4.0.9 version: 4.0.10 @@ -240,8 +153,8 @@ importers: packages/agentos: dependencies: '@agentos-software/common': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/common + version: link:../../../secure-exec/registry/software/common '@rivet-dev/agentos-core': specifier: workspace:* version: link:../core @@ -283,8 +196,8 @@ importers: specifier: workspace:* version: link:../core '@secure-exec/sandbox': - specifier: 'catalog:' - version: 0.3.2(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6) + specifier: link:../../../secure-exec/registry/tool/sandbox + version: link:../../../secure-exec/registry/tool/sandbox sandbox-agent: specifier: ^0.4.2 version: 0.4.2(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6) @@ -293,8 +206,8 @@ importers: version: 4.3.6 devDependencies: '@agentos-software/common': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/common + version: link:../../../secure-exec/registry/software/common '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -327,8 +240,8 @@ importers: packages/core: dependencies: '@agentos-software/common': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/common + version: link:../../../secure-exec/registry/software/common '@aws-sdk/client-s3': specifier: ^3.1019.0 version: 3.1020.0 @@ -339,8 +252,8 @@ importers: specifier: ^0.6.2 version: 0.6.2 '@secure-exec/core': - specifier: 'catalog:' - version: 0.3.2 + specifier: link:../../../secure-exec/packages/core + version: link:../../../secure-exec/packages/core '@xterm/headless': specifier: ^6.0.0 version: 6.0.0 @@ -370,71 +283,71 @@ importers: version: 3.25.2(zod@4.3.6) devDependencies: '@agentos-software/claude-code': - specifier: 'catalog:' - version: 0.0.0-nathan-binding-workspace.9be0a88(@cfworker/json-schema@4.1.1) + specifier: link:../../../secure-exec/registry/agent/claude + version: link:../../../secure-exec/registry/agent/claude '@agentos-software/codex': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/agent/codex + version: link:../../../secure-exec/registry/agent/codex '@agentos-software/codex-cli': - specifier: 'catalog:' - version: 0.0.0-codex-claude-runtime-fixes.9cbef3a + specifier: link:../../../secure-exec/registry/software/codex + version: link:../../../secure-exec/registry/software/codex '@agentos-software/coreutils': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/coreutils + version: link:../../../secure-exec/registry/software/coreutils '@agentos-software/curl': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/curl + version: link:../../../secure-exec/registry/software/curl '@agentos-software/diffutils': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/diffutils + version: link:../../../secure-exec/registry/software/diffutils '@agentos-software/fd': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/fd + version: link:../../../secure-exec/registry/software/fd '@agentos-software/file': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/file + version: link:../../../secure-exec/registry/software/file '@agentos-software/findutils': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/findutils + version: link:../../../secure-exec/registry/software/findutils '@agentos-software/gawk': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/gawk + version: link:../../../secure-exec/registry/software/gawk '@agentos-software/git': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/git + version: link:../../../secure-exec/registry/software/git '@agentos-software/grep': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/grep + version: link:../../../secure-exec/registry/software/grep '@agentos-software/gzip': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/gzip + version: link:../../../secure-exec/registry/software/gzip '@agentos-software/jq': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/jq + version: link:../../../secure-exec/registry/software/jq '@agentos-software/opencode': - specifier: 'catalog:' - version: 0.0.0-nathan-binding-workspace.9be0a88 + specifier: link:../../../secure-exec/registry/agent/opencode + version: link:../../../secure-exec/registry/agent/opencode '@agentos-software/pi': - specifier: 'catalog:' - version: 0.0.0-nathan-binding-workspace.9be0a88(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.20.0(bufferutil@4.1.0))(zod@4.3.6) + specifier: link:../../../secure-exec/registry/agent/pi + version: link:../../../secure-exec/registry/agent/pi '@agentos-software/pi-cli': - specifier: 'catalog:' - version: 0.0.0-nathan-binding-workspace.9be0a88(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.20.0(bufferutil@4.1.0))(zod@4.3.6) + specifier: link:../../../secure-exec/registry/agent/pi-cli + version: link:../../../secure-exec/registry/agent/pi-cli '@agentos-software/ripgrep': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/ripgrep + version: link:../../../secure-exec/registry/software/ripgrep '@agentos-software/sed': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/sed + version: link:../../../secure-exec/registry/software/sed '@agentos-software/tar': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/tar + version: link:../../../secure-exec/registry/software/tar '@agentos-software/tree': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/tree + version: link:../../../secure-exec/registry/software/tree '@agentos-software/yq': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/yq + version: link:../../../secure-exec/registry/software/yq '@anthropic-ai/claude-agent-sdk': specifier: ^0.2.87 version: 0.2.87(@cfworker/json-schema@4.1.1)(zod@4.3.6) @@ -463,11 +376,11 @@ importers: specifier: link:../agentos-sandbox version: link:../agentos-sandbox '@secure-exec/google-drive': - specifier: 'catalog:' - version: 0.3.2 + specifier: link:../../../secure-exec/registry/file-system/google-drive + version: link:../../../secure-exec/registry/file-system/google-drive '@secure-exec/s3': - specifier: 'catalog:' - version: 0.3.2 + specifier: link:../../../secure-exec/registry/file-system/s3 + version: link:../../../secure-exec/registry/file-system/s3 '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -490,34 +403,6 @@ importers: specifier: npm:zod@^3.25.76 version: zod@3.25.76 - packages/dev-shell: - dependencies: - '@agentos-software/common': - specifier: 'catalog:' - version: 0.3.0-rc.2 - '@rivet-dev/agentos-core': - specifier: workspace:* - version: link:../core - pino: - specifier: ^10.3.1 - version: 10.3.1 - devDependencies: - '@types/node': - specifier: ^22.19.3 - version: 22.19.15 - '@xterm/headless': - specifier: ^6.0.0 - version: 6.0.0 - tsx: - specifier: ^4.19.2 - version: 4.21.0 - typescript: - specifier: ^5.7.2 - version: 5.9.3 - vitest: - specifier: ^2.1.8 - version: 2.1.9(@types/node@22.19.15) - packages/playground: dependencies: '@rivet-dev/agentos-browser': @@ -549,8 +434,8 @@ importers: packages/posix: dependencies: '@secure-exec/core': - specifier: 'catalog:' - version: 0.3.2 + specifier: link:../../../secure-exec/packages/core + version: link:../../../secure-exec/packages/core devDependencies: '@types/node': specifier: ^22.10.2 @@ -571,8 +456,8 @@ importers: packages/python: dependencies: '@secure-exec/core': - specifier: 'catalog:' - version: 0.3.2 + specifier: link:../../../secure-exec/packages/core + version: link:../../../secure-exec/packages/core pyodide: specifier: ^0.28.3 version: 0.28.3(bufferutil@4.1.0) @@ -653,38 +538,83 @@ importers: packages/shell: dependencies: '@agentos-software/codex-cli': - specifier: 'catalog:' - version: 0.0.0-codex-claude-runtime-fixes.9cbef3a - '@agentos-software/common': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/codex + version: link:../../../secure-exec/registry/software/codex + '@agentos-software/coreutils': + specifier: link:../../../secure-exec/registry/software/coreutils + version: link:../../../secure-exec/registry/software/coreutils + '@agentos-software/curl': + specifier: link:../../../secure-exec/registry/software/curl + version: link:../../../secure-exec/registry/software/curl + '@agentos-software/diffutils': + specifier: link:../../../secure-exec/registry/software/diffutils + version: link:../../../secure-exec/registry/software/diffutils + '@agentos-software/duckdb': + specifier: link:../../../secure-exec/registry/software/duckdb + version: link:../../../secure-exec/registry/software/duckdb '@agentos-software/fd': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/fd + version: link:../../../secure-exec/registry/software/fd '@agentos-software/file': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/file + version: link:../../../secure-exec/registry/software/file + '@agentos-software/findutils': + specifier: link:../../../secure-exec/registry/software/findutils + version: link:../../../secure-exec/registry/software/findutils + '@agentos-software/gawk': + specifier: link:../../../secure-exec/registry/software/gawk + version: link:../../../secure-exec/registry/software/gawk + '@agentos-software/git': + specifier: link:../../../secure-exec/registry/software/git + version: link:../../../secure-exec/registry/software/git + '@agentos-software/grep': + specifier: link:../../../secure-exec/registry/software/grep + version: link:../../../secure-exec/registry/software/grep + '@agentos-software/gzip': + specifier: link:../../../secure-exec/registry/software/gzip + version: link:../../../secure-exec/registry/software/gzip + '@agentos-software/http-get': + specifier: link:../../../secure-exec/registry/software/http-get + version: link:../../../secure-exec/registry/software/http-get '@agentos-software/jq': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/jq + version: link:../../../secure-exec/registry/software/jq + '@agentos-software/make': + specifier: link:../../../secure-exec/registry/software/make + version: link:../../../secure-exec/registry/software/make '@agentos-software/ripgrep': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/ripgrep + version: link:../../../secure-exec/registry/software/ripgrep + '@agentos-software/sed': + specifier: link:../../../secure-exec/registry/software/sed + version: link:../../../secure-exec/registry/software/sed + '@agentos-software/sqlite3': + specifier: link:../../../secure-exec/registry/software/sqlite3 + version: link:../../../secure-exec/registry/software/sqlite3 + '@agentos-software/tar': + specifier: link:../../../secure-exec/registry/software/tar + version: link:../../../secure-exec/registry/software/tar '@agentos-software/tree': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/tree + version: link:../../../secure-exec/registry/software/tree '@agentos-software/unzip': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/unzip + version: link:../../../secure-exec/registry/software/unzip + '@agentos-software/wget': + specifier: link:../../../secure-exec/registry/software/wget + version: link:../../../secure-exec/registry/software/wget '@agentos-software/yq': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/yq + version: link:../../../secure-exec/registry/software/yq '@agentos-software/zip': - specifier: 'catalog:' - version: 0.3.0-rc.2 + specifier: link:../../../secure-exec/registry/software/zip + version: link:../../../secure-exec/registry/software/zip '@rivet-dev/agentos-core': specifier: workspace:* version: link:../core + commander: + specifier: ^14.0.2 + version: 14.0.3 devDependencies: '@types/node': specifier: ^22.19.3 @@ -796,84 +726,6 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - '@agentos-software/claude-code@0.0.0-nathan-binding-workspace.9be0a88': - resolution: {integrity: sha512-lSrWgqisbNFV3kkUpcyjH67UZsI/BWAWqlzr/B5SrHN0FcQU/1xVqYp+ooz24HdUuLlpp4Otx5sJHfb1NLRvDQ==} - hasBin: true - - '@agentos-software/codex-cli@0.0.0-codex-claude-runtime-fixes.9cbef3a': - resolution: {integrity: sha512-vxJ0T2h9O46oOzohDKChKZfduLGelpKz3p7AoatRUmz2n4pIntCcyvUyry6LeX0jcg1W5n0HaI5fX2b4C450Xw==} - - '@agentos-software/codex@0.3.0-rc.2': - resolution: {integrity: sha512-DKAb9Qs1+zV1sgtMEqVo7K60R4IryKMY24jZdH5NX5cKhIi5ED1o+AufINtNSMJxdrByz+MuX+2HkvWXCvHYgw==} - - '@agentos-software/common@0.3.0-rc.2': - resolution: {integrity: sha512-LWMGMFwjBbJDjbIAE8+hMDbbrOptxgXcgwJ6Ve5eFn0ncI6+T0fH5Fr3uTLZyxh0AUhSejbKDVjHwdQczFKnGQ==} - - '@agentos-software/coreutils@0.3.0-rc.2': - resolution: {integrity: sha512-NoGMK0RMTAWnoTBOCAjR7SAsumS8Sx/k8P+/Tg3VPnqy5kdIbMWF3z+3Tw3B626hQB+cuZhgj4nOHcO8UxrHjQ==} - - '@agentos-software/curl@0.3.0-rc.2': - resolution: {integrity: sha512-TRSzT29858IvHxQ6IxgyTNPNlLchmjtLhtqFss2UnKv/dRFK65/piouF0Ho53N2b0lvB3+eOkwV/A4zBLOqAuA==} - - '@agentos-software/diffutils@0.3.0-rc.2': - resolution: {integrity: sha512-0nL1mauroKHPD+HIQ0S+2SnYD4Z7h+IpCED9AlMnbuhCfN6ePjXUSUuuPOTJplcIUrFlT2RWC0OM7Q/0zifyXA==} - - '@agentos-software/fd@0.3.0-rc.2': - resolution: {integrity: sha512-Tdu0oJ/3gqPdrQKRl9tukUnRPD3ner7ZjMoStjnowlPCtLWYw+EGbH1yMBDlDInkrwM5ybzD8vUY6NoGJqTj4A==} - - '@agentos-software/file@0.3.0-rc.2': - resolution: {integrity: sha512-fK8EiM/QX9Rr5KBUsCViO3m5Myz/dA3sMYDVvC3D4H+/FqrzujW1BmBjYv3H3QwH9X1GsC9EpTI0MPE0QJzmtQ==} - - '@agentos-software/findutils@0.3.0-rc.2': - resolution: {integrity: sha512-v1HQbKHhhX4tC9HRYcV57COpYuZJfx8E8GBG2NAC2sj/fDTwhK9vnuNbRLWAy5kdnV/fK3vurXIst7DZfxSFUg==} - - '@agentos-software/gawk@0.3.0-rc.2': - resolution: {integrity: sha512-GJYYNhRulRGXw6mKwB+0/1pkrMeS78aSXcB/wM8IuWpcFCb9NLkZENbDa9Mnc5Bpf3GKKhRsqtvgIEQ4KvxHFg==} - - '@agentos-software/git@0.3.0-rc.2': - resolution: {integrity: sha512-fF6gMwRbkx77CeHKyuRigtasO/pHycQjGfJ2p5KK+/ygBXZJbpH3DyizLl1lBzARBYFhpcIDjLTr2GAKNLkV0A==} - - '@agentos-software/grep@0.3.0-rc.2': - resolution: {integrity: sha512-OwTEYN2fBUK18LYyJzHp0ryIxBINBuAYHI/RGuSCrH7ljKX9dLYYlLmqayMV33IAP7fpzV5s9s+Ad6wNxe19gQ==} - - '@agentos-software/gzip@0.3.0-rc.2': - resolution: {integrity: sha512-ylm/j9fqL5ykRxVZiGcwCDfwKUCb7nhP77bk5d4mX77vb/y0t/guX5CdAMoCEJbAUa7RZVxawKct2H+53TMWEg==} - - '@agentos-software/jq@0.3.0-rc.2': - resolution: {integrity: sha512-xNLHt7m7OaCPOQDX5OlocxqG0dPoCv2JCKkGld2W3uD3oOsO70WZl9sbfNBf8n3doHKgN2Xs04DGVhA/Ynabiw==} - - '@agentos-software/opencode@0.0.0-nathan-binding-workspace.9be0a88': - resolution: {integrity: sha512-CW4rrRKVLpnWY5pLM+JCLTROries+TMMLMIlr0XMwh07fcEJI0Asq8Z93mSmVR1uCFNcWJbB6NM7CRxcaxTq4w==} - hasBin: true - - '@agentos-software/pi-cli@0.0.0-nathan-binding-workspace.9be0a88': - resolution: {integrity: sha512-ga8fnQNr8pmkWS+MfCqPAMa97aFd/xkoL56Fowb5IGZ878vYkhlUMPagO4LBwNcTDpfmJVX16VSqBP76tLiHmw==} - - '@agentos-software/pi@0.0.0-nathan-binding-workspace.9be0a88': - resolution: {integrity: sha512-An4QkRI3K4uPonsjh6V85o0J/qUC7qJuVPaoSVP2KYT7DhDuQ2IYjrgrV6ggcBbDOKlMUIMY/m8GlPa4Skbatw==} - hasBin: true - - '@agentos-software/ripgrep@0.3.0-rc.2': - resolution: {integrity: sha512-OsT641m1kXkGON6D8HZaqlOPnqBN2O3MI1Uvqn+//s9n0MuQDmZ5ojg/jIaaSCTjadXOeFcrJzSASP/lajrxzA==} - - '@agentos-software/sed@0.3.0-rc.2': - resolution: {integrity: sha512-1Pgyt+ZUjBo2Bwdrq2unONw4+9xMmKnRE5xBm/jE7ECfN2L2JX4j7YvcKsN7j1wHrsnjlMuIZfyEBZpqbmH1eQ==} - - '@agentos-software/tar@0.3.0-rc.2': - resolution: {integrity: sha512-0pHAV+sf945JTPdFypp3Dml1k2v1sPK82el6u4PEJWVStuZ3FJl9N+joqdb6ozkIjhmLd7Yk5Dw+sZw6zUtzdg==} - - '@agentos-software/tree@0.3.0-rc.2': - resolution: {integrity: sha512-3hx2P8YOA9ndDPoNV6+ZN8UF75o/O6Uf2IDKCOQ06beamMnrCia1vADOZ2dakDRBYQY5wvf14ISQg0Vc8rTT+Q==} - - '@agentos-software/unzip@0.3.0-rc.2': - resolution: {integrity: sha512-2MrIUpyekfSmcDpG9lXUI5tPrOZx+BdK0WxN2PkgV90ZxUEUnCI1s71IUfMB+4tr6BaO28ZQQCUFW0PkcvpNyQ==} - - '@agentos-software/yq@0.3.0-rc.2': - resolution: {integrity: sha512-FfuElsvnBJXphb7v3MchMrvLpLrI31cOrrPd6KlIR1jvFb6Lv0aV39ksC19OFJDdnHrueutze9LXGDiXxJaGcA==} - - '@agentos-software/zip@0.3.0-rc.2': - resolution: {integrity: sha512-q4A7d/XegHccQV5PboGGDlKKEZZ+89XNWq/pSy6Fu9WWdwChwaO2EAyZdiaQ64+Biu8hRoXlgKuLsripeuEUpw==} - '@ai-sdk/amazon-bedrock@3.0.93': resolution: {integrity: sha512-57cP3Ume6DdQP05xPYl2g554EqPrQgKRW/eE3BGm1ktK1k71e35HGzNl1GZHIYKct82QrY/iQuheanSonI88Dg==} engines: {node: '>=18'} @@ -2782,49 +2634,9 @@ packages: '@secure-exec/core@0.2.1': resolution: {integrity: sha512-HsnUv6gClpMA1BBRmX86j30TKTZtgJC/fO1tVavr7IpM2zNKbHU8LgSlBd7mv2SNy02ImTmU/GnQ3aYB4NSbEg==} - '@secure-exec/core@0.3.2': - resolution: {integrity: sha512-9uKk6/pZsnnTVZJMy1WQuWDyPeYCbBDoUbHZ8LoLnWILGYAKcIOBi5ulbkCDc/7GeKF7kguyEsdQKa9soRHBqQ==} - - '@secure-exec/google-drive@0.3.2': - resolution: {integrity: sha512-R0Wg/7/rfX/HNbALEL78GXL5B7eCA2d+agymJr0XUMV2A6Ia2P2W3Gi2uGaJqlat5i543n6+kJ4lmnvPIRWcDg==} - '@secure-exec/nodejs@0.2.1': resolution: {integrity: sha512-UJMJqVFxexlHJV0Q9nWURvrz6GElj8673DDOOFln6FHR6JS+9SaSU3eISrN158DuNC3SFi4rgjb/scKnK4YOYQ==} - '@secure-exec/s3@0.3.2': - resolution: {integrity: sha512-WlNLvDfb+9k/Qg8hfFcFQqrqhoIX0ncdFo14eGnwBo805bm1m5D8eO4ekaONdmhNJAcq86HwOjOtTWcwXtq0qg==} - - '@secure-exec/sandbox@0.3.2': - resolution: {integrity: sha512-9CLG9jP1ZS5a9jLPI57UK+hidfe7H/ZGfQ+nK/fgCMt509kR+oM17EA73CqyzR+W9r3I43KwEaG7hXLgViNPJw==} - - '@secure-exec/sidecar-darwin-arm64@0.3.2': - resolution: {integrity: sha512-cpgQpTS6bdeu7VmGoMB6p9CURrHFredjAAi6j/a7y7j+xIGiG+W27tUqA8bPQs910LX2GGI16WiPqEF3ylajYA==} - engines: {node: '>=20'} - cpu: [arm64] - os: [darwin] - - '@secure-exec/sidecar-darwin-x64@0.3.2': - resolution: {integrity: sha512-jPbeTBrK6K87ja+sfUwbSv//w6Wn4OMlwHFh150sswdmVE1pChBpcMu4MYJ+H9zPAQWrg7C48JcvygOIvg9hFw==} - engines: {node: '>=20'} - cpu: [x64] - os: [darwin] - - '@secure-exec/sidecar-linux-arm64-gnu@0.3.2': - resolution: {integrity: sha512-dupP5Ihfspx1RtJ1lpiYy0mw4G3XUvP+fNQPGBsEUT+lIv1BvHmp6icOE7e0sjGsIdtxTEzy/4M8z31v7s2qFQ==} - engines: {node: '>=20'} - cpu: [arm64] - os: [linux] - - '@secure-exec/sidecar-linux-x64-gnu@0.3.2': - resolution: {integrity: sha512-EM+mxnATz/8u6+XdJS3OtvA1f44FoRnmp7gjUvNK4/nvfAnLjbdr7da6YDFcZh0m7jgEfpMIt0W4V/wENewv8g==} - engines: {node: '>=20'} - cpu: [x64] - os: [linux] - - '@secure-exec/sidecar@0.3.2': - resolution: {integrity: sha512-AYhEtPzs+CWfUhNL9TXLISp8K1ugNY3KE/HDxfQOvDcu/pxULVaUdVnQ8/HENT2AoU+NJT9s0lJuHic4XMVEAg==} - engines: {node: '>=20'} - '@secure-exec/v8-darwin-arm64@0.2.1': resolution: {integrity: sha512-gEWhMHzUpLwzuBNAD0lVkZXE8wFlWMLp4IOZ+56FYwOW/C+m07cYxuW4TjHyPqZ+vPm3IkoaMqqH5yT9VhjX/Q==} cpu: [arm64] @@ -5775,10 +5587,6 @@ packages: pino-std-serializers@7.1.0: resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} - pino@10.3.1: - resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} - hasBin: true - pino@9.14.0: resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} hasBin: true @@ -6495,10 +6303,6 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} - thread-stream@4.0.0: - resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} - engines: {node: '>=20'} - through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -7133,100 +6937,6 @@ snapshots: dependencies: zod: 4.3.6 - '@agentos-software/claude-code@0.0.0-nathan-binding-workspace.9be0a88(@cfworker/json-schema@4.1.1)': - dependencies: - '@agentclientprotocol/sdk': 0.16.1(zod@4.3.6) - '@anthropic-ai/claude-agent-sdk': 0.2.87(@cfworker/json-schema@4.1.1)(zod@4.3.6) - '@rivet-dev/agentos-core': link:packages/core - zod: 4.3.6 - transitivePeerDependencies: - - '@cfworker/json-schema' - - supports-color - - '@agentos-software/codex-cli@0.0.0-codex-claude-runtime-fixes.9cbef3a': {} - - '@agentos-software/codex@0.3.0-rc.2': {} - - '@agentos-software/common@0.3.0-rc.2': - dependencies: - '@agentos-software/coreutils': 0.3.0-rc.2 - '@agentos-software/diffutils': 0.3.0-rc.2 - '@agentos-software/findutils': 0.3.0-rc.2 - '@agentos-software/gawk': 0.3.0-rc.2 - '@agentos-software/grep': 0.3.0-rc.2 - '@agentos-software/gzip': 0.3.0-rc.2 - '@agentos-software/sed': 0.3.0-rc.2 - '@agentos-software/tar': 0.3.0-rc.2 - - '@agentos-software/coreutils@0.3.0-rc.2': {} - - '@agentos-software/curl@0.3.0-rc.2': {} - - '@agentos-software/diffutils@0.3.0-rc.2': {} - - '@agentos-software/fd@0.3.0-rc.2': {} - - '@agentos-software/file@0.3.0-rc.2': {} - - '@agentos-software/findutils@0.3.0-rc.2': {} - - '@agentos-software/gawk@0.3.0-rc.2': {} - - '@agentos-software/git@0.3.0-rc.2': {} - - '@agentos-software/grep@0.3.0-rc.2': {} - - '@agentos-software/gzip@0.3.0-rc.2': {} - - '@agentos-software/jq@0.3.0-rc.2': {} - - '@agentos-software/opencode@0.0.0-nathan-binding-workspace.9be0a88': - dependencies: - '@rivet-dev/agentos-core': link:packages/core - - '@agentos-software/pi-cli@0.0.0-nathan-binding-workspace.9be0a88(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.20.0(bufferutil@4.1.0))(zod@4.3.6)': - dependencies: - '@mariozechner/pi-coding-agent': 0.60.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.20.0(bufferutil@4.1.0))(zod@4.3.6) - '@rivet-dev/agentos-core': link:packages/core - pi-acp: 0.0.23 - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - aws-crt - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@agentos-software/pi@0.0.0-nathan-binding-workspace.9be0a88(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.20.0(bufferutil@4.1.0))(zod@4.3.6)': - dependencies: - '@agentclientprotocol/sdk': 0.16.1(zod@4.3.6) - '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.20.0(bufferutil@4.1.0))(zod@4.3.6) - '@mariozechner/pi-coding-agent': 0.60.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.20.0(bufferutil@4.1.0))(zod@4.3.6) - '@rivet-dev/agentos-core': link:packages/core - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - aws-crt - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@agentos-software/ripgrep@0.3.0-rc.2': {} - - '@agentos-software/sed@0.3.0-rc.2': {} - - '@agentos-software/tar@0.3.0-rc.2': {} - - '@agentos-software/tree@0.3.0-rc.2': {} - - '@agentos-software/unzip@0.3.0-rc.2': {} - - '@agentos-software/yq@0.3.0-rc.2': {} - - '@agentos-software/zip@0.3.0-rc.2': {} - '@ai-sdk/amazon-bedrock@3.0.93(zod@4.3.6)': dependencies: '@ai-sdk/anthropic': 2.0.74(zod@4.3.6) @@ -9521,16 +9231,6 @@ snapshots: dependencies: better-sqlite3: 12.8.0 - '@secure-exec/core@0.3.2': - dependencies: - '@rivetkit/bare-ts': 0.6.2 - '@secure-exec/sidecar': 0.3.2 - zod: 4.3.6 - - '@secure-exec/google-drive@0.3.2': - dependencies: - '@secure-exec/core': 0.3.2 - '@secure-exec/nodejs@0.2.1': dependencies: '@secure-exec/core': 0.2.1 @@ -9542,45 +9242,6 @@ snapshots: node-stdlib-browser: 1.3.1 web-streams-polyfill: 4.2.0 - '@secure-exec/s3@0.3.2': - dependencies: - '@secure-exec/core': 0.3.2 - - '@secure-exec/sandbox@0.3.2(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6)': - dependencies: - '@secure-exec/core': 0.3.2 - sandbox-agent: 0.4.2(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6) - transitivePeerDependencies: - - '@cloudflare/sandbox' - - '@daytonaio/sdk' - - '@e2b/code-interpreter' - - '@fly/sprites' - - '@vercel/sandbox' - - computesdk - - dockerode - - get-port - - modal - - zod - - '@secure-exec/sidecar-darwin-arm64@0.3.2': - optional: true - - '@secure-exec/sidecar-darwin-x64@0.3.2': - optional: true - - '@secure-exec/sidecar-linux-arm64-gnu@0.3.2': - optional: true - - '@secure-exec/sidecar-linux-x64-gnu@0.3.2': - optional: true - - '@secure-exec/sidecar@0.3.2': - optionalDependencies: - '@secure-exec/sidecar-darwin-arm64': 0.3.2 - '@secure-exec/sidecar-darwin-x64': 0.3.2 - '@secure-exec/sidecar-linux-arm64-gnu': 0.3.2 - '@secure-exec/sidecar-linux-x64-gnu': 0.3.2 - '@secure-exec/v8-darwin-arm64@0.2.1': optional: true @@ -13294,20 +12955,6 @@ snapshots: pino-std-serializers@7.1.0: {} - pino@10.3.1: - dependencies: - '@pinojs/redact': 0.4.0 - atomic-sleep: 1.0.0 - on-exit-leak-free: 2.1.2 - pino-abstract-transport: 3.0.0 - pino-std-serializers: 7.1.0 - process-warning: 5.0.0 - quick-format-unescaped: 4.0.4 - real-require: 0.2.0 - safe-stable-stringify: 2.5.0 - sonic-boom: 4.2.1 - thread-stream: 4.0.0 - pino@9.14.0: dependencies: '@pinojs/redact': 0.4.0 @@ -14343,10 +13990,6 @@ snapshots: dependencies: real-require: 0.2.0 - thread-stream@4.0.0: - dependencies: - real-require: 0.2.0 - through@2.3.8: optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 87f136aa5..241001664 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,7 +3,6 @@ packages: - packages/agentos - packages/browser - packages/core - - packages/dev-shell - packages/playground - packages/posix - packages/python