diff --git a/CLAUDE.md b/CLAUDE.md index 28418089c..2ec5f97e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,7 @@ Every bound that protects a shared resource — memory/heap, CPU/wall-clock, fd/ ## Build And Assets - The VM base filesystem artifact is derived from Alpine Linux, but runtime source should stay generic. -- Rebuild the base filesystem with `pnpm --dir packages/build-tools snapshot:alpine-defaults`, then `pnpm --dir packages/build-tools build:base-filesystem`. +- Rebuild the base filesystem (requires Docker) with `pnpm --dir packages/build-tools build:base-filesystem`. The one script snapshots Alpine, applies the secure-exec transforms, and writes the single canonical `packages/core/fixtures/base-filesystem.json`, mirroring the same bytes into the crate-vendored `crates/sidecar/assets/` and `crates/vfs/assets/` copies (those exist only as the `cargo publish` fallback; never hand-edit them). - The V8 bridge bundle is generated from `packages/build-tools/scripts/build-v8-bridge.mjs`; keep its generated assets aligned with bridge-contract changes. - `registry/native` owns the Rust-to-WASM command build; package-local `registry/software/*/wasm/` output is release material. diff --git a/crates/execution/src/javascript.rs b/crates/execution/src/javascript.rs index 8c62e8482..031a8b010 100644 --- a/crates/execution/src/javascript.rs +++ b/crates/execution/src/javascript.rs @@ -1808,12 +1808,26 @@ impl JavascriptExecutionEngine { // Build user code: prefer inline code, fall back to entrypoint-based let translator = GuestPathTranslator::from_request(&request); - let host_entrypoint = translator.resolve_host_entrypoint(&request.cwd, &request.argv[0]); - let guest_entrypoint = if request.argv[0] == "-e" || request.argv[0] == "--eval" { + let mut host_entrypoint = + translator.resolve_host_entrypoint(&request.cwd, &request.argv[0]); + let mut guest_entrypoint = if request.argv[0] == "-e" || request.argv[0] == "--eval" { request.argv[0].clone() } else { translator.host_to_guest_string(&host_entrypoint) }; + // Run an `/opt/agentos` projected command from its REALPATH: the + // `/opt/agentos/bin/` entrypoint is a symlink into the package, so + // following it (like Node's default `preserveSymlinks=false`) makes the + // runtime read the real file — its `import`s resolve against the package's + // own `node_modules` and its module mode is read from the real + // `package.json`. Scoped to `/opt/agentos` so the legacy + // `/root/node_modules` (pnpm) flow is untouched. + if guest_entrypoint.starts_with("/opt/agentos/") { + if let Ok(canonical) = std::fs::canonicalize(&host_entrypoint) { + guest_entrypoint = translator.host_to_guest_string(&canonical); + host_entrypoint = canonical; + } + } let process_argv = if matches!(guest_entrypoint.as_str(), "-e" | "--eval") { std::iter::once(String::from("node")) .chain(request.argv.iter().skip(1).cloned()) @@ -1834,12 +1848,21 @@ impl JavascriptExecutionEngine { .is_some_and(inline_code_uses_module_mode); if !matches!(guest_entrypoint.as_str(), "-e" | "--eval") && !use_module_mode { if let Some(inline_code) = inline_code.as_ref() { - if let Some(parent) = host_entrypoint.parent() { - fs::create_dir_all(parent) + // Only materialize the import cache for a SYNTHESIZED entrypoint + // (no file on disk). Never overwrite an existing entrypoint: it may + // be a read-only mounted package command (e.g. `/opt/agentos/bin/*`), + // and writing the shebang-stripped `inline_code` over it corrupts + // the file so a second exec sees no `#!` and fails with ENOEXEC. + // `require()` strips the shebang in-process, so the on-disk file + // keeps its shebang and stays re-executable. + if !host_entrypoint.exists() { + if let Some(parent) = host_entrypoint.parent() { + fs::create_dir_all(parent) + .map_err(JavascriptExecutionError::PrepareImportCache)?; + } + fs::write(&host_entrypoint, inline_code) .map_err(JavascriptExecutionError::PrepareImportCache)?; } - fs::write(&host_entrypoint, inline_code) - .map_err(JavascriptExecutionError::PrepareImportCache)?; } } let user_code = if matches!(guest_entrypoint.as_str(), "-e" | "--eval") { @@ -2210,9 +2233,16 @@ fn build_v8_user_code(entrypoint: &str, env: &BTreeMap) -> Strin } fn host_entrypoint_uses_module_mode(entrypoint: &Path) -> bool { - match entrypoint.extension().and_then(|ext| ext.to_str()) { + // Follow symlinks before deciding: an `/opt/agentos/bin/` command is an + // extensionless symlink into the package (e.g. → `.../dist/adapter.js`), so + // the real file's extension AND its nearest `package.json` "type" must be read + // from the resolved target, not the symlink. Without this, an ESM agent + // adapter loads as CommonJS and crashes with "Cannot use import statement + // outside a module". + let resolved = std::fs::canonicalize(entrypoint).unwrap_or_else(|_| entrypoint.to_path_buf()); + match resolved.extension().and_then(|ext| ext.to_str()) { Some("mjs" | "mts") => true, - Some("js") => nearest_package_json_type(entrypoint).as_deref() == Some("module"), + Some("js") => nearest_package_json_type(&resolved).as_deref() == Some("module"), _ => false, } } @@ -3452,16 +3482,20 @@ impl<'a, R: ModuleFsReader> ModuleResolver<'a, R> { } let source = self.reader.read_to_string(path)?; - Some( - if matches!( - Path::new(path).extension().and_then(|ext| ext.to_str()), - Some("js" | "mjs" | "cjs") - ) { - strip_javascript_hashbang(&source) - } else { - source - }, - ) + // Strip a leading `#!` shebang for JS modules AND for any extensionless + // executable entrypoint that carries one (e.g. a `/opt/agentos/bin/*` + // package command named without a `.js` suffix). Node strips shebangs + // regardless of extension; `strip_javascript_hashbang` is a no-op when the + // source has none, so this never affects shebang-free files. + let has_js_extension = matches!( + Path::new(path).extension().and_then(|ext| ext.to_str()), + Some("js" | "mjs" | "cjs") + ); + Some(if has_js_extension || source.starts_with("#!") { + strip_javascript_hashbang(&source) + } else { + source + }) } pub fn module_format(&mut self, path: &str) -> Option { @@ -6030,6 +6064,7 @@ fn builtin_named_exports(module_name: &str) -> &'static [&'static str] { "chmodSync", "closeSync", "constants", + "copyFileSync", "createReadStream", "createWriteStream", "existsSync", diff --git a/crates/execution/src/lib.rs b/crates/execution/src/lib.rs index dcfc695cf..408489719 100644 --- a/crates/execution/src/lib.rs +++ b/crates/execution/src/lib.rs @@ -34,9 +34,9 @@ pub use secure_exec_bridge::GuestRuntime; pub use secure_exec_v8_runtime::execution::GuestModuleReader; pub use signal::{NodeSignalDispositionAction, NodeSignalHandlerRegistration}; pub use wasm::{ - CreateWasmContextRequest, NativeBinaryFormat, StartWasmExecutionRequest, WasmContext, - WasmExecution, WasmExecutionEngine, WasmExecutionError, WasmExecutionEvent, - WasmExecutionLimits, WasmExecutionResult, WasmPermissionTier, + detect_native_binary_format, CreateWasmContextRequest, NativeBinaryFormat, + StartWasmExecutionRequest, WasmContext, WasmExecution, WasmExecutionEngine, WasmExecutionError, + WasmExecutionEvent, WasmExecutionLimits, WasmExecutionResult, WasmPermissionTier, }; pub trait NativeExecutionBridge: secure_exec_bridge::ExecutionBridge {} diff --git a/crates/execution/src/wasm.rs b/crates/execution/src/wasm.rs index 535077836..2c627c4b4 100644 --- a/crates/execution/src/wasm.rs +++ b/crates/execution/src/wasm.rs @@ -55,7 +55,7 @@ const DEFAULT_WASM_GUEST_HOME: &str = "/root"; const DEFAULT_WASM_GUEST_USER: &str = "root"; const DEFAULT_WASM_GUEST_SHELL: &str = "/bin/sh"; const DEFAULT_WASM_GUEST_PATH: &str = - "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; + "/usr/local/sbin:/usr/local/bin:/opt/agentos/bin:/usr/sbin:/usr/bin:/sbin:/bin"; // Warmup is a best-effort compile-cache optimization; fall back to a cold start // instead of burning minutes on a stalled prewarm session. const DEFAULT_WASM_PREWARM_TIMEOUT_MS: u64 = 30_000; @@ -5134,7 +5134,7 @@ fn verify_wasm_module_header( }) } -fn detect_native_binary_format(header: &[u8]) -> Option { +pub fn detect_native_binary_format(header: &[u8]) -> Option { if header.len() >= 4 && &header[..4] == b"\x7fELF" { return Some(NativeBinaryFormat::Elf); } diff --git a/crates/execution/tests/python.rs b/crates/execution/tests/python.rs index 82f6b4e7b..f6479c113 100644 --- a/crates/execution/tests/python.rs +++ b/crates/execution/tests/python.rs @@ -725,6 +725,9 @@ export async function loadPyodide(options) { | PythonVfsRpcMethod::SubprocessRun => { panic!("unexpected non-filesystem Python RPC: {:?}", request.method) } + other => { + panic!("unexpected Python VFS RPC method in this test: {other:?}") + } } } Some(PythonExecutionEvent::JavascriptSyncRpcRequest(request)) => { diff --git a/crates/kernel/src/command_registry.rs b/crates/kernel/src/command_registry.rs index a2b4d1dbe..1f3ee57a0 100644 --- a/crates/kernel/src/command_registry.rs +++ b/crates/kernel/src/command_registry.rs @@ -83,13 +83,6 @@ impl CommandRegistry { .collect() } - pub fn populate_bin(&self, vfs: &mut F) -> VfsResult<()> - where - F: VirtualFileSystem, - { - self.populate_commands(vfs, self.commands.keys()) - } - pub fn populate_driver_bin(&self, vfs: &mut F, driver: &CommandDriver) -> VfsResult<()> where F: VirtualFileSystem, diff --git a/crates/kernel/src/kernel.rs b/crates/kernel/src/kernel.rs index 86383b105..7b18efd0a 100644 --- a/crates/kernel/src/kernel.rs +++ b/crates/kernel/src/kernel.rs @@ -3105,19 +3105,8 @@ impl KernelVm { return Err(KernelError::command_not_found(command)); }; - if let Some(registered_command) = self.resolve_registered_command_path(&path) { - let driver = self - .commands - .resolve(®istered_command) - .cloned() - .ok_or_else(|| KernelError::command_not_found(®istered_command))?; - return Ok(ResolvedSpawnCommand { - command: registered_command, - args: args.to_vec(), - driver, - }); - } - + // A resolved file is executed by its header (Linux binfmt), not mapped + // back to a registered command name via hardcoded /bin prefixes. let shebang = self .parse_shebang_command(&path)? .ok_or_else(|| KernelError::new("ENOEXEC", format!("exec format error: {path}")))?; @@ -3129,7 +3118,24 @@ impl KernelVm { command: &str, cwd: &str, ) -> KernelResult> { + // A command with no slash is resolved by a real `$PATH` walk (Linux + // `execvp`): the first PATH entry holding an executable file wins; a + // present-but-non-executable match is skipped (so the search may end in + // ENOENT, not EACCES). An empty PATH element means the cwd. This is what + // makes `/opt/agentos/bin` packages resolvable by bare name. if !command.contains('/') { + let path_env = self.env.get("PATH").cloned().unwrap_or_default(); + for entry in path_env.split(':') { + let dir = if entry.is_empty() { cwd } else { entry }; + let candidate = normalize_path(&format!("{dir}/{command}")); + let Ok(stat) = self.filesystem.stat(&candidate) else { + continue; + }; + if stat.is_directory || stat.mode & EXECUTABLE_PERMISSION_BITS == 0 { + continue; + } + return Ok(Some(candidate)); + } return Ok(None); } @@ -3154,29 +3160,6 @@ impl KernelVm { Ok(Some(path)) } - fn resolve_registered_command_path(&self, path: &str) -> Option { - let normalized = normalize_path(path); - for prefix in ["/bin/", "/usr/bin/", "/usr/local/bin/"] { - let Some(name) = normalized.strip_prefix(prefix) else { - continue; - }; - if !name.is_empty() && !name.contains('/') && self.commands.resolve(name).is_some() { - return Some(name.to_owned()); - } - } - - if let Some(name) = normalized - .strip_prefix("/__secure_exec/commands/") - .and_then(|suffix| suffix.rsplit('/').next()) - { - if !name.is_empty() && !name.contains('/') && self.commands.resolve(name).is_some() { - return Some(name.to_owned()); - } - } - - None - } - fn parse_shebang_command(&mut self, path: &str) -> KernelResult> { let header = self.filesystem.pread(path, 0, SHEBANG_LINE_MAX_BYTES + 1)?; if !header.starts_with(b"#!") { @@ -3224,10 +3207,17 @@ impl KernelVm { )); } interpreter_args.remove(0) - } else if let Some(command) = self.resolve_registered_command_path(&interpreter) { - command } else if self.commands.resolve(&shebang.interpreter).is_some() { shebang.interpreter + } else if let Some(name) = interpreter + .rsplit('/') + .next() + .filter(|name| !name.is_empty() && self.commands.resolve(name).is_some()) + { + // Resolve the interpreter by its basename (e.g. `/bin/sh` -> `sh`), + // which replaces the legacy 3-prefix `resolve_registered_command_path` + // with the Linux-like "the command is the file's basename" rule. + name.to_owned() } else { return Err(KernelError::command_not_found(&shebang.interpreter)); }; diff --git a/crates/kernel/tests/command_registry.rs b/crates/kernel/tests/command_registry.rs index e50d69f4d..0d680611a 100644 --- a/crates/kernel/tests/command_registry.rs +++ b/crates/kernel/tests/command_registry.rs @@ -77,14 +77,17 @@ fn records_warning_when_overriding_existing_command() { } #[test] -fn populate_bin_creates_stub_entries() { +fn populate_driver_bin_creates_stub_entries() { let mut vfs = MemoryFileSystem::new(); let mut registry = CommandRegistry::new(); + let driver = CommandDriver::new("wasmvm", ["grep", "cat"]); registry - .register(CommandDriver::new("wasmvm", ["grep", "cat"])) + .register(driver.clone()) .expect("register commands"); - registry.populate_bin(&mut vfs).expect("populate /bin"); + registry + .populate_driver_bin(&mut vfs, &driver) + .expect("populate /bin"); assert!(vfs.exists("/bin/grep")); assert!(vfs.exists("/bin/cat")); @@ -157,7 +160,11 @@ fn kernel_driver_registration_rejects_command_path_names_without_writing_stubs() } #[test] -fn mounted_agentos_command_paths_resolve_to_registered_drivers() { +fn path_resolved_files_dispatch_by_header_not_registered_name() { + // The legacy 3-prefix `resolve_registered_command_path` is gone: a file + // resolved by path is executed by its header (binfmt), not mapped back to a + // registered command name. A `#!/bin/sh` stub therefore resolves to the `sh` + // interpreter (by basename) rather than to its own filename. let mut config = KernelVmConfig::new("vm-mounted-command-path"); config.permissions = Permissions::allow_all(); let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); @@ -191,6 +198,7 @@ fn mounted_agentos_command_paths_resolve_to_registered_drivers() { .get(&process.pid()) .cloned() .expect("process info"); - assert_eq!(info.command, "xu"); + // Dispatched by the `#!/bin/sh` header → the `sh` interpreter, on the wasmvm driver. + assert_eq!(info.command, "sh"); assert_eq!(info.driver, "wasmvm"); } diff --git a/crates/sidecar/assets/base-filesystem.json b/crates/sidecar/assets/base-filesystem.json deleted file mode 100644 index 64e03bfa5..000000000 --- a/crates/sidecar/assets/base-filesystem.json +++ /dev/null @@ -1,536 +0,0 @@ -{ - "source": { - "snapshotPath": "alpine-defaults.json", - "image": "alpine:3.22", - "snapshotCreatedAt": "2026-06-22T19:45:20.529Z", - "builtAt": "2026-06-23T03:47:12.644Z", - "transforms": [ - "Normalize HOSTNAME to secure-exec", - "Preserve the captured user-level environment and filesystem layout as the secure-exec base layer", - "Add the non-Alpine /workspace directory (default agent working directory) owned by the base user" - ] - }, - "environment": { - "env": { - "CHARSET": "UTF-8", - "HOME": "/home/agentos", - "HOSTNAME": "secure-exec", - "LANG": "C.UTF-8", - "LC_COLLATE": "C", - "LOGNAME": "agentos", - "PAGER": "less", - "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", - "SHELL": "/bin/sh", - "USER": "agentos" - }, - "prompt": "\\h:\\w\\$ " - }, - "filesystem": { - "entries": [ - { - "path": "/", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/bin", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/alpine-release", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "3.22.4\n" - }, - { - "path": "/etc/apk", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/busybox-paths.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/crontabs", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/group", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "root:x:0:root\nbin:x:1:root,bin,daemon\ndaemon:x:2:root,bin,daemon\nsys:x:3:root,bin\nadm:x:4:root,daemon\ntty:x:5:\ndisk:x:6:root\nlp:x:7:lp\nkmem:x:9:\nwheel:x:10:root\nfloppy:x:11:root\nmail:x:12:mail\nnews:x:13:news\nuucp:x:14:uucp\ncron:x:16:cron\naudio:x:18:\ncdrom:x:19:\ndialout:x:20:root\nftp:x:21:\nsshd:x:22:\ninput:x:23:\ntape:x:26:root\nvideo:x:27:root\nnetdev:x:28:\nkvm:x:34:kvm\ngames:x:35:\nshadow:x:42:\nwww-data:x:82:\nusers:x:100:games\nntp:x:123:\nabuild:x:300:\nutmp:x:406:\nping:x:999:\nnogroup:x:65533:\nnobody:x:65534:\nagentos:x:1000:\n" - }, - { - "path": "/etc/hostname", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "secure-exec\n" - }, - { - "path": "/etc/logrotate.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/modprobe.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/modules-load.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/network", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/nsswitch.conf", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "# musl itself does not support NSS, however some third-party DNS\n# implementations use the nsswitch.conf file to determine what\n# policy to follow.\n# Editing this file is not recommended.\nhosts: files dns\n" - }, - { - "path": "/etc/opt", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/os-release", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "../usr/lib/os-release" - }, - { - "path": "/etc/passwd", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "root:x:0:0:root:/root:/bin/sh\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaemon:x:2:2:daemon:/sbin:/sbin/nologin\nlp:x:4:7:lp:/var/spool/lpd:/sbin/nologin\nsync:x:5:0:sync:/sbin:/bin/sync\nshutdown:x:6:0:shutdown:/sbin:/sbin/shutdown\nhalt:x:7:0:halt:/sbin:/sbin/halt\nmail:x:8:12:mail:/var/mail:/sbin/nologin\nnews:x:9:13:news:/usr/lib/news:/sbin/nologin\nuucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin\ncron:x:16:16:cron:/var/spool/cron:/sbin/nologin\nftp:x:21:21::/var/lib/ftp:/sbin/nologin\nsshd:x:22:22:sshd:/dev/null:/sbin/nologin\ngames:x:35:35:games:/usr/games:/sbin/nologin\nntp:x:123:123:NTP:/var/empty:/sbin/nologin\nguest:x:405:100:guest:/dev/null:/sbin/nologin\nnobody:x:65534:65534:nobody:/:/sbin/nologin\nagentos:x:1000:1000::/home/agentos:/bin/sh\n" - }, - { - "path": "/etc/periodic", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/profile", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "export PATH=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n\nexport PAGER=less\numask 022\n\n# use nicer PS1 for bash and busybox ash\nif [ -n \"$BASH_VERSION\" -o \"$BB_ASH_VERSION\" ]; then\n\tPS1='\\h:\\w\\$ '\n# use nicer PS1 for zsh\nelif [ -n \"$ZSH_VERSION\" ]; then\n\tPS1='%m:%~%# '\n# set up fallback default PS1\nelse\n\t: \"${HOSTNAME:=$(hostname)}\"\n\tPS1='${HOSTNAME%%.*}:$PWD'\n\t[ \"$(id -u)\" -eq 0 ] && PS1=\"${PS1}# \" || PS1=\"${PS1}\\$ \"\nfi\n\nfor script in /etc/profile.d/*.sh ; do\n\tif [ -r \"$script\" ] ; then\n\t\t. \"$script\"\n\tfi\ndone\nunset script\n" - }, - { - "path": "/etc/profile.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/secfixes.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/shadow", - "type": "file", - "mode": "640", - "uid": 0, - "gid": 42, - "content": "root:*::0:::::\nbin:!::0:::::\ndaemon:!::0:::::\nlp:!::0:::::\nsync:!::0:::::\nshutdown:!::0:::::\nhalt:!::0:::::\nmail:!::0:::::\nnews:!::0:::::\nuucp:!::0:::::\ncron:!::0:::::\nftp:!::0:::::\nsshd:!::0:::::\ngames:!::0:::::\nntp:!::0:::::\nguest:!::0:::::\nnobody:!::0:::::\nagentos:!:20626:0:99999:7:::\n" - }, - { - "path": "/etc/shells", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "# valid login shells\n/bin/sh\n/bin/ash\n" - }, - { - "path": "/etc/ssl", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/ssl1.1", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/sysctl.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/udhcpc", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/home", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/home/agentos", - "type": "directory", - "mode": "2755", - "uid": 1000, - "gid": 1000 - }, - { - "path": "/lib", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/lib/apk", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/lib/firmware", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/lib/modules-load.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/lib/sysctl.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/media", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/media/cdrom", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/media/floppy", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/media/usb", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/mnt", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/opt", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/root", - "type": "directory", - "mode": "700", - "uid": 0, - "gid": 0 - }, - { - "path": "/run", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/run/lock", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/sbin", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/srv", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/sys", - "type": "directory", - "mode": "555", - "uid": 0, - "gid": 0 - }, - { - "path": "/tmp", - "type": "directory", - "mode": "1777", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/bin", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/bin/env", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "/bin/busybox" - }, - { - "path": "/usr/lib", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/lib/os-release", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "NAME=\"Alpine Linux\"\nID=alpine\nVERSION_ID=3.22.4\nPRETTY_NAME=\"Alpine Linux v3.22\"\nHOME_URL=\"https://alpinelinux.org/\"\nBUG_REPORT_URL=\"https://gitlab.alpinelinux.org/alpine/aports/-/issues\"\n" - }, - { - "path": "/usr/local", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/sbin", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/share", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/cache", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/empty", - "type": "directory", - "mode": "555", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/lib", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/local", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/lock", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "../run/lock" - }, - { - "path": "/var/log", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/mail", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/opt", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/run", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "../run" - }, - { - "path": "/var/spool", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/spool/cron", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/spool/cron/crontabs", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "../../../etc/crontabs" - }, - { - "path": "/var/tmp", - "type": "directory", - "mode": "1777", - "uid": 0, - "gid": 0 - }, - { - "path": "/workspace", - "type": "directory", - "mode": "755", - "uid": 1000, - "gid": 1000 - } - ] - } -} diff --git a/crates/sidecar/build.rs b/crates/sidecar/build.rs index c87a22b39..b45dd647b 100644 --- a/crates/sidecar/build.rs +++ b/crates/sidecar/build.rs @@ -1,8 +1,8 @@ use std::{env, fs, path::PathBuf}; -// Stage the base filesystem fixture into OUT_DIR. In-tree builds use the -// canonical secure-exec package fixture from the current workspace; the -// published crate falls back to the vendored `assets/base-filesystem.json` copy. +// The base filesystem snapshot is owned + embedded by the vfs crate; the sidecar +// reads it via `vfs::posix::base_filesystem_json()`, so this build script only +// generates the sidecar wire protocol. fn main() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set")); @@ -10,28 +10,6 @@ fn main() { println!("cargo:rerun-if-changed=build.rs"); generate_protocol(&manifest_dir, &out_dir); - - let workspace_fixtures = [ - manifest_dir.join("../../packages/secure-exec-core/fixtures/base-filesystem.json"), - manifest_dir.join("../../packages/core/fixtures/base-filesystem.json"), - ]; - let vendored = manifest_dir.join("assets/base-filesystem.json"); - let src = workspace_fixtures - .into_iter() - .find(|fixture| fixture.exists()) - .unwrap_or(vendored); - - println!("cargo:rerun-if-changed={}", src.display()); - - let dest = out_dir.join("base-filesystem.json"); - fs::copy(&src, &dest).unwrap_or_else(|error| { - panic!( - "failed to stage base-filesystem.json from {} to {}: {}", - src.display(), - dest.display(), - error - ) - }); } fn generate_protocol(manifest_dir: &std::path::Path, out_dir: &std::path::Path) { diff --git a/crates/sidecar/protocol/secure_exec_sidecar_v1.bare b/crates/sidecar/protocol/secure_exec_sidecar_v1.bare index 2cc288415..e588d96a7 100644 --- a/crates/sidecar/protocol/secure_exec_sidecar_v1.bare +++ b/crates/sidecar/protocol/secure_exec_sidecar_v1.bare @@ -208,6 +208,16 @@ type WasmPermissionTier enum { ISOLATED } +# agentOS package descriptor. The sidecar projects the self-contained `dir` +# read-only under `//` and links its `bin/` +# commands onto $PATH. `dir` is a trusted host path (the client configures its +# own VM); the sidecar is the host-side TCB that builds the staging tree. +type PackageDescriptor struct { + name: str + dir: str + acpEntrypoint: optional +} + type ConfigureVmRequest struct { mounts: list software: list @@ -217,6 +227,12 @@ type ConfigureVmRequest struct { projectedModules: list commandPermissions: map loopbackExemptPorts: list + packages: list + packagesMountAt: str +} + +type LinkPackageRequest struct { + package: PackageDescriptor } type RegisteredHostCallbackExample struct { @@ -413,7 +429,8 @@ type RequestPayload union { PersistenceLoadRequest | PersistenceFlushRequest | VmFetchRequest | - ExtEnvelope + ExtEnvelope | + LinkPackageRequest } type RequestFrame struct { @@ -451,6 +468,10 @@ type VmConfiguredResponse struct { appliedSoftware: u32 } +type PackageLinkedResponse struct { + commands: list +} + type HostCallbacksRegisteredResponse struct { registration: str commandCount: u32 @@ -656,7 +677,8 @@ type ResponsePayload union { PersistenceFlushedResponse | RejectedResponse | VmFetchResponse | - ExtEnvelope + ExtEnvelope | + PackageLinkedResponse } type ResponseFrame struct { diff --git a/crates/sidecar/src/bootstrap.rs b/crates/sidecar/src/bootstrap.rs index 762f6e7eb..7daa0ab32 100644 --- a/crates/sidecar/src/bootstrap.rs +++ b/crates/sidecar/src/bootstrap.rs @@ -23,10 +23,9 @@ use secure_exec_kernel::vfs::VirtualFileSystem; use serde::Deserialize; use std::collections::BTreeMap; -// Staged into OUT_DIR by build.rs (canonical workspace fixture in-tree, or the -// vendored `assets/base-filesystem.json` copy in the published crate). -const BUNDLED_BASE_FILESYSTEM_JSON: &[u8] = - include_bytes!(concat!(env!("OUT_DIR"), "/base-filesystem.json")); +// Single Rust-side source: the bytes are owned + embedded by the vfs crate, so +// the sidecar reads them through it instead of carrying its own vendored copy. +const BUNDLED_BASE_FILESYSTEM_JSON: &[u8] = vfs::posix::base_filesystem_json().as_bytes(); pub(crate) fn build_root_filesystem( descriptor: &RootFilesystemDescriptor, @@ -161,27 +160,44 @@ where pub(crate) fn discover_command_guest_paths(kernel: &mut SidecarKernel) -> BTreeMap { let mut command_guest_paths = BTreeMap::new(); - let Ok(command_roots) = kernel.read_dir("/__secure_exec/commands") else { - return command_guest_paths; - }; - let mut ordered_roots = command_roots - .into_iter() - .filter(|entry| !entry.is_empty() && entry.chars().all(|ch| ch.is_ascii_digit())) - .collect::>(); - ordered_roots.sort(); + // Legacy command-dir staging: `/__secure_exec/commands//` (numbered roots, + // earliest-root-wins). Kept for compat with VMs that still stage WASM command dirs. + if let Ok(command_roots) = kernel.read_dir("/__secure_exec/commands") { + let mut ordered_roots = command_roots + .into_iter() + .filter(|entry| !entry.is_empty() && entry.chars().all(|ch| ch.is_ascii_digit())) + .collect::>(); + ordered_roots.sort(); + + for root in ordered_roots { + let guest_root = format!("/__secure_exec/commands/{root}"); + let Ok(entries) = kernel.read_dir(&guest_root) else { + continue; + }; - for root in ordered_roots { - let guest_root = format!("/__secure_exec/commands/{root}"); - let Ok(entries) = kernel.read_dir(&guest_root) else { - continue; - }; + for entry in entries { + if entry.starts_with('.') || command_guest_paths.contains_key(&entry) { + continue; + } + command_guest_paths.insert(entry.clone(), format!("{guest_root}/{entry}")); + } + } + } + // New `{name,dir}` package projection: the sidecar links each package's commands into + // `/opt/agentos/bin` (on `$PATH`). Register them as guest commands too, so the kernel + // routes a bare `cat`/`sh` to the WASM/JS execution driver instead of falling through + // to PATH+shebang resolution (a `\0asm` WASM file has no shebang → ENOEXEC otherwise). + if let Ok(entries) = kernel.read_dir(crate::package_projection::OPT_AGENTOS_BIN) { for entry in entries { if entry.starts_with('.') || command_guest_paths.contains_key(&entry) { continue; } - command_guest_paths.insert(entry.clone(), format!("{guest_root}/{entry}")); + command_guest_paths.insert( + entry.clone(), + format!("{}/{entry}", crate::package_projection::OPT_AGENTOS_BIN), + ); } } diff --git a/crates/sidecar/src/execution.rs b/crates/sidecar/src/execution.rs index a659b6335..d74a7470d 100644 --- a/crates/sidecar/src/execution.rs +++ b/crates/sidecar/src/execution.rs @@ -94,7 +94,9 @@ use rustls::{ }; use scrypt::{scrypt, Params as ScryptParams}; use secure_exec_bridge::LifecycleState; -use secure_exec_execution::wasm::WasmExecutionError; +use secure_exec_execution::wasm::{ + detect_native_binary_format, NativeBinaryFormat, WasmExecutionError, +}; use secure_exec_execution::{ javascript::handle_internal_bridge_call_from_host_context, v8_host::V8SessionHandle, v8_runtime, CreateJavascriptContextRequest, CreatePythonContextRequest, @@ -5060,7 +5062,7 @@ where process_id: &str, request: PythonVfsRpcRequest, ) -> Result<(), SidecarError> { - if self.vms.get(vm_id).is_none() { + if !self.vms.contains_key(vm_id) { return Ok(()); } let response = self.python_socket_op(vm_id, process_id, &request); @@ -8309,6 +8311,42 @@ fn resolve_command_execution( }); let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint); + // Dispatch a resolved command FILE by its header (binfmt), exactly like Linux: + // `\0asm` magic → WASM, `#!…node` → JS, `#!…python3` → Python, native → ENOEXEC. + match classify_executable_header(&host_entrypoint) { + // Reject native ELF/Mach-O/PE binaries up front (no native-arch handler), exactly + // as a Linux kernel returns ENOEXEC for a foreign binary format — before the file is + // ever handed to the WASM runtime, where it would otherwise fail late at warmup. + HeaderRuntime::Native(format) => { + return Err(SidecarError::Execution( + WasmExecutionError::NativeBinaryNotSupported { + path: host_entrypoint.clone(), + header: read_executable_header_bytes(&host_entrypoint).unwrap_or_default(), + format, + } + .to_string(), + )); + } + // A `#!…python3` script file routes to the Python runtime — symmetric with the + // `#!…node` → JS path below. Synthesize `python3 ` so the script + // path becomes the interpreter's first positional argument. The resolved + // execution (`runtime: Python, entrypoint: `) is identical to a direct + // `python3 foo.py` invocation, so it shares that runtime path exactly. + HeaderRuntime::Python => { + let mut python_args = Vec::with_capacity(args.len() + 1); + python_args.push(guest_entrypoint.clone()); + python_args.extend(args.iter().cloned()); + return resolve_python_command_execution( + vm, + "python3", + &python_args, + env, + guest_cwd, + host_cwd, + ); + } + _ => {} + } if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) = resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint) { @@ -8412,12 +8450,12 @@ fn resolve_javascript_command_entrypoint_inner( let script = load_executable_script_preview(host_entrypoint)?; let interpreter = parse_script_interpreter_name(&script); - if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) { + if interpreter.is_none() && is_javascript_entrypoint_by_extension(host_entrypoint) { return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf())); } let interpreter = interpreter?; - if interpreter == "node" { + if GuestRuntimeKind::from_interpreter(&interpreter) == Some(GuestRuntimeKind::JavaScript) { return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf())); } @@ -8446,6 +8484,80 @@ fn load_executable_script_preview(path: &Path) -> Option { Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned()) } +impl GuestRuntimeKind { + /// The single string→enum boundary: map a shebang interpreter basename to a guest + /// runtime, so file-dispatch sites match on the enum instead of comparing raw strings. + /// Magic-byte runtimes (`WebAssembly`, native) and `#!/bin/sh` npm shims are not + /// interpreter names and are classified separately. + fn from_interpreter(basename: &str) -> Option { + match basename { + "node" | "nodejs" => Some(Self::JavaScript), + "python" | "python3" => Some(Self::Python), + _ => None, + } + } +} + +/// One runtime classification of a resolved executable, decided by its header (binfmt): +/// magic bytes for WASM/native, the shebang interpreter (via [`GuestRuntimeKind::from_interpreter`]) +/// for scripts, the `#!/bin/sh` npm wrapper, or nothing recognizable. +#[derive(Debug, Clone, PartialEq, Eq)] +enum HeaderRuntime { + Wasm, + Js, + Python, + Native(NativeBinaryFormat), + ShellShim, + Unknown, +} + +/// The single header→runtime classifier: read the file's leading bytes once and map them +/// to exactly one [`HeaderRuntime`], so dispatch sites match an enum instead of comparing +/// raw strings/magics in scattered places. +fn classify_executable_header(path: &Path) -> HeaderRuntime { + let Some(header) = read_executable_header_bytes(path) else { + return HeaderRuntime::Unknown; + }; + if header.starts_with(b"\0asm") { + return HeaderRuntime::Wasm; + } + if let Some(format) = detect_native_binary_format(&header) { + return HeaderRuntime::Native(format); + } + if !header.starts_with(b"#!") { + return HeaderRuntime::Unknown; + } + let Some(preview) = load_executable_script_preview(path) else { + return HeaderRuntime::Unknown; + }; + let Some(interpreter) = parse_script_interpreter_name(&preview) else { + return HeaderRuntime::Unknown; + }; + match GuestRuntimeKind::from_interpreter(&interpreter) { + Some(GuestRuntimeKind::JavaScript) => HeaderRuntime::Js, + Some(GuestRuntimeKind::Python) => HeaderRuntime::Python, + // `from_interpreter` never yields WebAssembly; `sh`/`bash`/`dash` are npm shell + // shims that redirect to a node target, everything else is unrecognized. + _ => { + if matches!(interpreter.as_str(), "sh" | "bash" | "dash") { + HeaderRuntime::ShellShim + } else { + HeaderRuntime::Unknown + } + } + } +} + +/// Read the leading raw bytes of an executable for magic-number dispatch. Raw (not +/// lossy UTF-8) so the WASM `\0asm` magic and native ELF/Mach-O/PE magics survive. +fn read_executable_header_bytes(path: &Path) -> Option> { + use std::io::Read as _; + let mut file = std::fs::File::open(path).ok()?; + let mut buf = [0u8; 8]; + let read = file.read(&mut buf).ok()?; + Some(buf[..read].to_vec()) +} + fn parse_script_interpreter_name(script: &str) -> Option { let shebang = script.lines().next()?.strip_prefix("#!")?.trim(); let mut tokens = shebang.split_whitespace(); @@ -8492,35 +8604,17 @@ fn parse_node_shell_shim_target(script: &str) -> Option { None } -fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool { - let extension = path - .extension() - .and_then(|value| value.to_str()) - .unwrap_or_default(); - if matches!(extension, "js" | "cjs" | "mjs") { - return true; - } - - if !path - .components() - .any(|component| component.as_os_str() == "node_modules") - { - return false; - } - - let preview = script.trim_start_matches('\u{feff}').trim_start(); - !preview.is_empty() - && !preview.starts_with("#!") - && (preview.starts_with("\"use strict\"") - || preview.starts_with("'use strict'") - || preview.starts_with("import ") - || preview.starts_with("export ") - || preview.starts_with("const ") - || preview.starts_with("let ") - || preview.starts_with("var ") - || preview.starts_with("Object.defineProperty(exports") - || preview.starts_with("module.exports") - || preview.starts_with("require(")) +/// A headerless file is treated as JavaScript only by its `.js`/`.cjs`/`.mjs` +/// extension (so headerless ESM still runs; R3). The legacy `node_modules` + +/// source-content heuristic was dropped per the spec — dispatch is by header +/// (shebang/magic) or extension, never by guessing from where a file lives. +fn is_javascript_entrypoint_by_extension(path: &Path) -> bool { + matches!( + path.extension() + .and_then(|value| value.to_str()) + .unwrap_or_default(), + "js" | "cjs" | "mjs" + ) } fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String { @@ -9048,11 +9142,14 @@ fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option } fn is_node_runtime_command(command: &str) -> bool { - matches!(command, "node" | "npm" | "npx") - || Path::new(command) - .file_name() - .and_then(|name| name.to_str()) - .is_some_and(|name| matches!(name, "node" | "npm" | "npx")) + let base = Path::new(command) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(command); + // `node`/`nodejs` route through the central interpreter→runtime map; `npm`/`npx` + // are node-family CLI names (not interpreters), kept explicit. + GuestRuntimeKind::from_interpreter(base) == Some(GuestRuntimeKind::JavaScript) + || matches!(base, "npm" | "npx") } fn python_command_base_name(command: &str) -> &str { @@ -9066,10 +9163,11 @@ fn python_command_base_name(command: &str) -> &str { /// served by the embedded Pyodide runtime, mirroring how `node` is served by the /// embedded V8 runtime. fn is_python_runtime_command(command: &str) -> bool { - matches!( - python_command_base_name(command), - "python" | "python3" | "pip" | "pip3" - ) + let base = python_command_base_name(command); + // `python`/`python3` route through the central interpreter→runtime map; `pip`/`pip3` + // are the Pyodide runtime's `python -m pip` aliases (not interpreter names). + GuestRuntimeKind::from_interpreter(base) == Some(GuestRuntimeKind::Python) + || matches!(base, "pip" | "pip3") } /// Parse a `python` / `pip` command line into a Pyodide execution. Supports the @@ -9350,19 +9448,26 @@ fn resolve_guest_command_entrypoint( } fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec { + let path = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)); + command_search_dirs_from_path(path, guest_cwd) +} + +/// The `$PATH` directory list, with exact POSIX/Linux semantics. Pure (no VM) so it can +/// be unit-tested. An **empty element** (a leading `:`, a trailing `:`, or `::`) means the +/// current working directory — the classic POSIX footgun, reproduced for fidelity. +fn command_search_dirs_from_path(path: Option<&str>, guest_cwd: &str) -> Vec { let mut search_dirs = Vec::new(); let mut seen = BTreeSet::new(); - if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) { + if let Some(path) = path { for segment in path.split(':') { - let trimmed = segment.trim(); - if trimmed.is_empty() { - continue; - } - let normalized = if trimmed.starts_with('/') { - normalize_path(trimmed) + let normalized = if segment.is_empty() { + // Empty element = cwd (POSIX). + normalize_path(guest_cwd) + } else if segment.starts_with('/') { + normalize_path(segment) } else { - normalize_path(&format!("{guest_cwd}/{trimmed}")) + normalize_path(&format!("{guest_cwd}/{segment}")) }; if seen.insert(normalized.clone()) { search_dirs.push(normalized); @@ -11788,10 +11893,10 @@ fn add_runtime_host_access_path( // discover_command_guest_paths moved to crate::bootstrap fn is_path_like_specifier(specifier: &str) -> bool { - specifier.starts_with('/') - || specifier.starts_with("./") - || specifier.starts_with("../") - || specifier.starts_with("file:") + // A command containing a '/' anywhere bypasses $PATH and is resolved relative to cwd + // (or absolute) — exactly as on Linux, where `execvp`/the shell skip the PATH search + // for any name with a slash (`/abs`, `./rel`, `../rel`, and also `subdir/cmd`). + specifier.contains('/') || specifier.starts_with("file:") } fn execution_wasm_permission_tier(tier: WasmPermissionTier) -> ExecutionWasmPermissionTier { @@ -22181,6 +22286,99 @@ pub(crate) fn ignore_stale_javascript_sync_rpc_response( mod error_code_tests { use super::{guest_errno_code, javascript_sync_rpc_error_code, SidecarError}; + #[test] + fn classify_executable_header_dispatches_by_magic_and_shebang() { + use super::{classify_executable_header, HeaderRuntime, NativeBinaryFormat}; + use std::fs; + use std::path::PathBuf; + + let dir = std::env::temp_dir().join(format!("aos-classify-{}", std::process::id())); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).unwrap(); + let write = |name: &str, bytes: &[u8]| -> PathBuf { + let p = dir.join(name); + fs::write(&p, bytes).unwrap(); + p + }; + + assert_eq!( + classify_executable_header(&write("w", b"\0asm\x01\0\0\0")), + HeaderRuntime::Wasm + ); + assert_eq!( + classify_executable_header(&write("e", b"\x7fELF\0\0\0\0")), + HeaderRuntime::Native(NativeBinaryFormat::Elf) + ); + assert_eq!( + classify_executable_header(&write("n", b"#!/usr/bin/env node\n")), + HeaderRuntime::Js + ); + assert_eq!( + classify_executable_header(&write("p", b"#!/usr/bin/env python3\n")), + HeaderRuntime::Python + ); + assert_eq!( + classify_executable_header(&write("s", b"#!/bin/sh\nexec node x\n")), + HeaderRuntime::ShellShim + ); + assert_eq!( + classify_executable_header(&write("t", b"plain text, no header\n")), + HeaderRuntime::Unknown + ); + assert_eq!( + classify_executable_header(&dir.join("does-not-exist")), + HeaderRuntime::Unknown + ); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn command_search_dirs_from_path_uses_exact_posix_semantics() { + use super::command_search_dirs_from_path as dirs; + let cwd = "/work"; + let fb = ["/bin", "/usr/bin", "/usr/local/bin"]; + + // Absolute elements, in order, then fallbacks. + assert_eq!( + dirs(Some("/a:/b"), cwd), + ["/a", "/b"] + .iter() + .chain(fb.iter()) + .map(|s| s.to_string()) + .collect::>() + ); + // Empty element (leading / trailing / `::`) = cwd. + assert_eq!(dirs(Some(":/a"), cwd)[0], "/work"); + assert!(dirs(Some("/a:"), cwd).contains(&String::from("/work"))); + assert_eq!( + dirs(Some("/a::/b"), cwd), + ["/a", "/work", "/b"] + .iter() + .chain(fb.iter()) + .map(|s| s.to_string()) + .collect::>() + ); + // Relative element resolved against cwd. + assert_eq!(dirs(Some("rel"), cwd)[0], "/work/rel"); + // Absent PATH = only fallbacks; duplicates deduped. + assert_eq!(dirs(None, cwd), fb.map(String::from).to_vec()); + assert_eq!(dirs(Some("/bin:/bin"), cwd), fb.map(String::from).to_vec()); + } + + #[test] + fn is_path_like_specifier_bypasses_path_for_any_slash() { + use super::is_path_like_specifier as plike; + // No slash → PATH-searched. + assert!(!plike("ls")); + assert!(!plike("python3")); + // Any slash → path-like (resolved relative to cwd / absolute), like Linux. + assert!(plike("/usr/bin/ls")); + assert!(plike("./run")); + assert!(plike("../tool")); + assert!(plike("subdir/cmd")); // previously mis-routed to PATH search + assert!(plike("file:x")); + } + #[test] fn guest_errno_code_rejects_guest_controlled_errno_segments() { assert_eq!(guest_errno_code("user said 'EACCES: denied'"), None); diff --git a/crates/sidecar/src/generated_protocol.rs b/crates/sidecar/src/generated_protocol.rs index 3b0c75f39..891f012fd 100644 --- a/crates/sidecar/src/generated_protocol.rs +++ b/crates/sidecar/src/generated_protocol.rs @@ -4,5 +4,9 @@ // lints are silenced for the generated code rather than editing build output. #![allow(dead_code)] #![allow(clippy::enum_variant_names)] +// pre-existing (main CI was red); not part of the package-resolution refactor. +// The generated protocol enums have a large dominant variant (ConfigureVmRequest); +// boxing would require touching the codegen, which is out of scope here. +#![allow(clippy::large_enum_variant)] include!(concat!(env!("OUT_DIR"), "/combined_imports.rs")); diff --git a/crates/sidecar/src/lib.rs b/crates/sidecar/src/lib.rs index 1cd861ce3..55bc25def 100644 --- a/crates/sidecar/src/lib.rs +++ b/crates/sidecar/src/lib.rs @@ -14,6 +14,7 @@ pub mod limits; #[cfg(target_os = "macos")] pub(crate) mod macos_fs; pub(crate) mod metadata; +pub mod package_projection; pub(crate) mod plugins; pub mod protocol; pub mod service; diff --git a/crates/sidecar/src/package_projection.rs b/crates/sidecar/src/package_projection.rs new file mode 100644 index 000000000..bb6643921 --- /dev/null +++ b/crates/sidecar/src/package_projection.rs @@ -0,0 +1,329 @@ +//! agentOS package projection (moved into the sidecar from the agent-os clients). +//! +//! A package is a self-contained directory produced by `@rivet-dev/agentos-toolchain +//! pack`; the sidecar projects it read-only under `/opt/agentos//` and +//! links its `bin/` commands into `/opt/agentos/bin` (which is on `$PATH`). A `current` +//! symlink gives an atomic version switch. The whole tree lives in ONE host staging dir +//! mounted at `/opt/agentos` — the VFS rejects cross-mount symlinks and confines host-dir +//! mounts with `RESOLVE_BENEATH`, so package content + `current` + the `bin/`/`man` farms +//! must share a single mount with only relative, in-tree symlinks. Because the host-dir +//! mount reflects host writes, appending to the staging dir adds commands to a running VM +//! live (the mechanism behind runtime `LinkPackage`). +//! +//! There is no `agentos-package.json` manifest: the package `name` (and optional +//! `acpEntrypoint`) come from the wire descriptor, the `version` comes from the package's +//! own root `package.json`, and the commands are derived from `bin/` (or `package.json` +//! "bin"). + +use std::collections::HashMap; +use std::fs; +use std::os::unix::fs::{symlink, PermissionsExt}; +use std::path::{Path, PathBuf}; +use std::sync::{Mutex, OnceLock}; + +use crate::state::SidecarError; + +/// Root of the agentOS package tree inside the VM. +pub const OPT_AGENTOS_ROOT: &str = "/opt/agentos"; +/// The symlink farm on `$PATH`. +pub const OPT_AGENTOS_BIN: &str = "/opt/agentos/bin"; + +/// A package to project, derived from the wire `PackageDescriptor`. +#[derive(Debug, Clone)] +pub struct PackageDescriptor { + pub name: String, + pub dir: String, + /// `bin/` command that speaks ACP, if this is an agent package. + pub acp_entrypoint: Option, +} + +fn io_err(context: &str, error: std::io::Error) -> SidecarError { + SidecarError::Io(format!("{context}: {error}")) +} + +/// Read the package's `version` from its root `package.json`. A toolchain-produced +/// package (flat or `--bundle`) always has a root `package.json {name,version,bin}`. +pub fn read_package_version(dir: &str) -> Result { + let path = Path::new(dir).join("package.json"); + if !path.exists() { + return Err(SidecarError::InvalidState(format!( + "missing required package.json in {dir} \ + (produce packages with '@rivet-dev/agentos-toolchain pack')" + ))); + } + let text = fs::read_to_string(&path).map_err(|e| io_err("read package.json", e))?; + let value: serde_json::Value = serde_json::from_str(&text) + .map_err(|e| SidecarError::InvalidState(format!("invalid package.json in {dir}: {e}")))?; + match value.get("version").and_then(|v| v.as_str()) { + Some(version) if !version.is_empty() => Ok(version.to_owned()), + _ => Err(SidecarError::InvalidState(format!( + "package.json in {dir} is missing a valid \"version\"" + ))), + } +} + +/// Map each command name to its entry path RELATIVE to the package root. +/// +/// A shipped package is an npm dependency, so it must not rely on `bin/` symlinks +/// (npm publish + cross-platform tooling strip/break them). Commands are therefore +/// declared in the root `package.json` "bin" map (command → real entry file). The +/// `/opt/agentos/bin/` symlink farm lives ONLY in the sidecar's host staging +/// dir and points at that entry. WASM packages instead ship a real `bin/` of +/// `.wasm` files, so fall back to the `bin/` directory when there is no +/// `package.json` "bin". +fn command_targets(dir: &str) -> Result, SidecarError> { + let pkg_json = Path::new(dir).join("package.json"); + if pkg_json.exists() { + if let Ok(text) = fs::read_to_string(&pkg_json) { + if let Ok(value) = serde_json::from_str::(&text) { + match value.get("bin") { + Some(serde_json::Value::String(path)) => { + if let Some(name) = value.get("name").and_then(|v| v.as_str()) { + let unscoped = name.rsplit('/').next().unwrap_or(name).to_owned(); + return Ok(vec![(unscoped, normalize_rel(path))]); + } + } + Some(serde_json::Value::Object(map)) => { + let mut targets: Vec<(String, String)> = map + .iter() + .filter_map(|(name, path)| { + path.as_str() + .map(|path| (name.clone(), normalize_rel(path))) + }) + .collect(); + targets.sort_by(|a, b| a.0.cmp(&b.0)); + return Ok(targets); + } + _ => {} + } + } + } + } + + let bin = Path::new(dir).join("bin"); + if bin.is_dir() { + let mut targets = Vec::new(); + for entry in fs::read_dir(&bin).map_err(|e| io_err("read bin/", e))? { + let entry = entry.map_err(|e| io_err("read bin/ entry", e))?; + if let Some(name) = entry.file_name().to_str() { + targets.push((name.to_owned(), format!("bin/{name}"))); + } + } + targets.sort_by(|a, b| a.0.cmp(&b.0)); + return Ok(targets); + } + Ok(Vec::new()) +} + +/// Strip a leading `./` so the resulting path is a clean in-package relative path. +fn normalize_rel(path: &str) -> String { + path.strip_prefix("./").unwrap_or(path).to_owned() +} + +/// Derive command names for the package (sorted). See [`command_targets`]. +pub fn derive_commands(dir: &str) -> Result, SidecarError> { + Ok(command_targets(dir)? + .into_iter() + .map(|(name, _)| name) + .collect()) +} + +/// Process-global shared-projection cache (Phase 5). Maps `@` to a host dir +/// holding ONE copy of that package's content; every VM's projection hardlinks from it +/// instead of re-copying. Keyed by name+version, so a version bump produces a fresh cache +/// entry (invalidation on version change). +fn projection_cache() -> &'static Mutex> { + static CACHE: OnceLock>> = OnceLock::new(); + CACHE.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// Copy a package's content to the shared cache once; return the cache dir. +fn cached_package_content( + desc_dir: &str, + name: &str, + version: &str, +) -> Result { + let key = format!("{name}@{version}"); + { + let cache = projection_cache() + .lock() + .expect("projection cache poisoned"); + if let Some(existing) = cache.get(&key) { + if existing.exists() { + return Ok(existing.clone()); + } + } + } + let dir = std::env::temp_dir().join(format!( + "agentos-pkgcache-{}-{}", + sanitize(name), + sanitize(version) + )); + let content = dir.join("content"); + if content.exists() { + let _ = fs::remove_dir_all(&content); + } + copy_tree_verbatim(Path::new(desc_dir), &content)?; + projection_cache() + .lock() + .expect("projection cache poisoned") + .insert(key, content.clone()); + Ok(content) +} + +fn sanitize(s: &str) -> String { + s.chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) + .collect() +} + +/// Recursively materialize `src` into `dst`, HARDLINKING regular files (shared inodes — no +/// data copy) and recreating symlinks/dirs. Falls back to a byte copy if hardlinking fails +/// (e.g. a cross-filesystem `EXDEV`). +fn hardlink_tree_from(src: &Path, dst: &Path) -> Result<(), SidecarError> { + let meta = fs::symlink_metadata(src).map_err(|e| io_err("stat cache source", e))?; + if meta.file_type().is_symlink() { + let target = fs::read_link(src).map_err(|e| io_err("read_link", e))?; + symlink(&target, dst).map_err(|e| io_err("symlink copy", e))?; + return Ok(()); + } + if meta.is_dir() { + fs::create_dir_all(dst).map_err(|e| io_err("create_dir", e))?; + for entry in fs::read_dir(src).map_err(|e| io_err("read_dir", e))? { + let entry = entry.map_err(|e| io_err("read_dir entry", e))?; + hardlink_tree_from(&entry.path(), &dst.join(entry.file_name()))?; + } + return Ok(()); + } + if fs::hard_link(src, dst).is_err() { + fs::copy(src, dst).map_err(|e| io_err("copy file", e))?; + } + Ok(()) +} + +/// Recursively copy `src` into `dst`, preserving symlinks verbatim (so relative in-package +/// links stay in-tree). Mirrors TS `cpSync({verbatimSymlinks:true})`. +fn copy_tree_verbatim(src: &Path, dst: &Path) -> Result<(), SidecarError> { + let meta = fs::symlink_metadata(src).map_err(|e| io_err("stat source", e))?; + if meta.file_type().is_symlink() { + let target = fs::read_link(src).map_err(|e| io_err("read_link", e))?; + symlink(&target, dst).map_err(|e| io_err("symlink copy", e))?; + return Ok(()); + } + if meta.is_dir() { + fs::create_dir_all(dst).map_err(|e| io_err("create_dir", e))?; + for entry in fs::read_dir(src).map_err(|e| io_err("read_dir", e))? { + let entry = entry.map_err(|e| io_err("read_dir entry", e))?; + copy_tree_verbatim(&entry.path(), &dst.join(entry.file_name()))?; + } + return Ok(()); + } + fs::copy(src, dst).map_err(|e| io_err("copy file", e))?; + Ok(()) +} + +/// Ensure the staging dir has a `bin/` so `/opt/agentos/bin` is a real (possibly empty) +/// directory on `$PATH`. Call once before any `link_package`. +pub fn init_projection(staging_root: &Path) -> Result<(), SidecarError> { + fs::create_dir_all(staging_root.join("bin")).map_err(|e| io_err("init projection bin/", e)) +} + +/// Add one package to the `/opt/agentos` staging dir. Returns the command names it linked. +/// Idempotent per command name (errors on a duplicate). +pub fn link_package( + desc: &PackageDescriptor, + staging_root: &Path, +) -> Result, SidecarError> { + let name = desc.name.clone(); + let version = read_package_version(&desc.dir)?; + let targets = command_targets(&desc.dir)?; + let commands: Vec = targets.iter().map(|(name, _)| name.clone()).collect(); + if let Some(acp) = &desc.acp_entrypoint { + if !commands.contains(acp) { + return Err(SidecarError::InvalidState(format!( + "agent acpEntrypoint {acp:?} is not one of {name}'s commands" + ))); + } + } + + let bin_dir = staging_root.join("bin"); + fs::create_dir_all(&bin_dir).map_err(|e| io_err("create bin/", e))?; + let name_dir = staging_root.join(&name); + let version_dir = name_dir.join(&version); + // Two meta-packages can both pull in the same sub-package (e.g. `build-essential` and + // `common` both include `coreutils`). Projecting an already-projected `/` + // is an idempotent no-op, not a conflict — its content + `bin/`/`man` links are already in + // the staging dir. (A *different* package re-providing a command still errors at the + // bin-link step below, which is the real duplicate-command case.) + if version_dir.exists() { + return Ok(commands); + } + // Hardlink content from a process-global cache (Phase 5: shared cross-VM projection) so + // a package is copied to disk ONCE and shared (same inodes) across every VM's read-only + // projection. Falls back to a copy across filesystems. + let cached = cached_package_content(&desc.dir, &name, &version)?; + hardlink_tree_from(&cached, &version_dir)?; + + // Toolchain-packed command files (npm `bin` scripts AND WASM `bin/*.wasm`) ship as + // plain `0644` data inside the npm tarball — npm never preserves an execute bit. The + // kernel's `$PATH` walk and exec(2) both require the execute bits (`0o111`), so a + // `0644` command would resolve to ENOENT (bare name skipped as non-executable) or + // EACCES (absolute path). Mark every projected command entry executable so the + // `/opt/agentos/bin` symlink farm points at runnable files. The entries are hardlinks + // into the shared cache, so this is idempotent across VMs (and a no-op on re-projection + // because `version_dir.exists()` short-circuits above). + for (_, entry) in &targets { + let entry_path = version_dir.join(entry); + if let Ok(meta) = fs::metadata(&entry_path) { + let mut perms = meta.permissions(); + let mode = perms.mode(); + perms.set_mode(mode | 0o111); + fs::set_permissions(&entry_path, perms) + .map_err(|e| io_err("chmod +x command entry", e))?; + } + } + + // /current -> + let current = name_dir.join("current"); + let _ = fs::remove_file(¤t); + symlink(&version, ¤t).map_err(|e| io_err("current symlink", e))?; + + // bin/ -> ..//current/ (the entry from package.json "bin", or + // bin/ for WASM packages). The symlink farm exists only in the staging dir. + for (cmd, entry) in &targets { + let dest = bin_dir.join(cmd); + if dest.exists() { + return Err(SidecarError::InvalidState(format!( + "command {cmd:?} is already provided by another package" + ))); + } + symlink(format!("../{name}/current/{entry}"), &dest) + .map_err(|e| io_err("bin symlink", e))?; + } + + // share/man/
/* -> ../../..//current/share/man/
/* + let man = version_dir.join("share").join("man"); + if man.is_dir() { + for section in fs::read_dir(&man).map_err(|e| io_err("read man/", e))? { + let section = section.map_err(|e| io_err("man section", e))?; + if !section.path().is_dir() { + continue; + } + let sec_name = section.file_name(); + let farm = staging_root.join("share").join("man").join(&sec_name); + fs::create_dir_all(&farm).map_err(|e| io_err("man farm dir", e))?; + for page in fs::read_dir(section.path()).map_err(|e| io_err("man pages", e))? { + let page = page.map_err(|e| io_err("man page", e))?; + let page_name = page.file_name(); + let target = format!( + "../../../{name}/current/share/man/{}/{}", + sec_name.to_string_lossy(), + page_name.to_string_lossy() + ); + symlink(target, farm.join(&page_name)).map_err(|e| io_err("man symlink", e))?; + } + } + } + + Ok(commands) +} diff --git a/crates/sidecar/src/protocol.rs b/crates/sidecar/src/protocol.rs index 55f0d8425..60aab6747 100644 --- a/crates/sidecar/src/protocol.rs +++ b/crates/sidecar/src/protocol.rs @@ -474,6 +474,9 @@ fn to_generated_request_payload( RequestPayload::Ext(inner) => { generated_protocol::RequestPayload::ExtEnvelope(to_generated_ext_envelope(inner)) } + RequestPayload::LinkPackage(inner) => { + generated_protocol::RequestPayload::LinkPackageRequest(inner.clone()) + } }) } @@ -570,6 +573,9 @@ fn from_generated_request_payload( generated_protocol::RequestPayload::ExtEnvelope(inner) => { RequestPayload::Ext(from_generated_ext_envelope(inner)) } + generated_protocol::RequestPayload::LinkPackageRequest(inner) => { + RequestPayload::LinkPackage(inner) + } }) } @@ -712,6 +718,9 @@ fn to_generated_response_payload( ResponsePayload::ExtResult(inner) => { generated_protocol::ResponsePayload::ExtEnvelope(to_generated_ext_envelope(inner)) } + ResponsePayload::PackageLinked(inner) => { + generated_protocol::ResponsePayload::PackageLinkedResponse(inner.clone()) + } }) } @@ -841,6 +850,9 @@ fn from_generated_response_payload( generated_protocol::ResponsePayload::ExtEnvelope(inner) => { ResponsePayload::ExtResult(from_generated_ext_envelope(inner)) } + generated_protocol::ResponsePayload::PackageLinkedResponse(inner) => { + ResponsePayload::PackageLinked(inner) + } }) } @@ -969,6 +981,9 @@ macro_rules! impl_bare_newtype_union_enum { $($variant:ident($ty:ty) = $tag:literal),+ $(,)? } ) => { + // pre-existing (main CI was red); not part of the package-resolution refactor. + // Dominant variant size predates this change; boxing is out of scope here. + #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] $(#[$json_attr])* enum $json_name { @@ -1063,6 +1078,9 @@ pub type ProtocolSchema = crate::wire::ProtocolSchema; pub type OwnershipScope = crate::wire::OwnershipScope; +// pre-existing (main CI was red); not part of the package-resolution refactor. +// Dominant variant size predates this change; boxing is out of scope here. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Eq)] pub enum ProtocolFrame { Request(RequestFrame), @@ -1173,6 +1191,9 @@ impl EventFrame { } } +// pre-existing (main CI was red); not part of the package-resolution refactor. +// Dominant variant (ConfigureVmRequest) predates this change; boxing is out of scope. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Eq)] pub enum RequestPayload { Authenticate(AuthenticateRequest), @@ -1204,6 +1225,7 @@ pub enum RequestPayload { PersistenceLoad(PersistenceLoadRequest), PersistenceFlush(PersistenceFlushRequest), Ext(ExtEnvelope), + LinkPackage(LinkPackageRequest), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1239,6 +1261,7 @@ pub enum ResponsePayload { PersistenceFlushed(PersistenceFlushedResponse), Rejected(RejectedResponse), ExtResult(ExtEnvelope), + PackageLinked(PackageLinkedResponse), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1351,6 +1374,12 @@ pub type SoftwareDescriptor = crate::wire::SoftwareDescriptor; pub type ProjectedModuleDescriptor = crate::wire::ProjectedModuleDescriptor; +pub type PackageDescriptor = crate::wire::PackageDescriptor; + +pub type LinkPackageRequest = crate::wire::LinkPackageRequest; + +pub type PackageLinkedResponse = crate::wire::PackageLinkedResponse; + pub type WasmPermissionTier = crate::wire::WasmPermissionTier; pub type ExecuteRequest = crate::wire::ExecuteRequest; @@ -1524,6 +1553,7 @@ impl_bare_newtype_union_enum!( PersistenceFlush(PersistenceFlushRequest) = 26, VmFetch(VmFetchRequest) = 27, Ext(ExtEnvelope) = 28, + LinkPackage(LinkPackageRequest) = 29, } ); @@ -1563,6 +1593,7 @@ impl_bare_newtype_union_enum!( Rejected(RejectedResponse) = 28, VmFetchResult(VmFetchResponse) = 29, ExtResult(ExtEnvelope) = 30, + PackageLinked(PackageLinkedResponse) = 31, } ); @@ -2139,6 +2170,7 @@ enum ExpectedResponseKind { PermissionDecision, PersistenceState, PersistenceFlushed, + PackageLinked, ExtResult, } @@ -2181,6 +2213,7 @@ impl ExpectedResponseKind { Self::PermissionDecision => "permission_decision", Self::PersistenceState => "persistence_state", Self::PersistenceFlushed => "persistence_flushed", + Self::PackageLinked => "package_linked", Self::ExtResult => "ext_result", } } @@ -2236,7 +2269,8 @@ impl RequestPayload { | Self::VmFetch(_) | Self::GetSignalState(_) | Self::GetZombieTimerCount(_) - | Self::HostFilesystemCall(_) => OwnershipRequirement::Vm, + | Self::HostFilesystemCall(_) + | Self::LinkPackage(_) => OwnershipRequirement::Vm, Self::Ext(_) => OwnershipRequirement::Any, } } @@ -2271,6 +2305,7 @@ impl RequestPayload { Self::HostFilesystemCall(_) => ExpectedResponseKind::FilesystemResult, Self::PersistenceLoad(_) => ExpectedResponseKind::PersistenceState, Self::PersistenceFlush(_) => ExpectedResponseKind::PersistenceFlushed, + Self::LinkPackage(_) => ExpectedResponseKind::PackageLinked, Self::Ext(_) => ExpectedResponseKind::ExtResult, } } @@ -2321,7 +2356,8 @@ impl ResponsePayload { | Self::SignalState(_) | Self::ZombieTimerCount(_) | Self::FilesystemResult(_) - | Self::PermissionDecision(_) => OwnershipRequirement::Vm, + | Self::PermissionDecision(_) + | Self::PackageLinked(_) => OwnershipRequirement::Vm, Self::ExtResult(_) => OwnershipRequirement::Any, } } @@ -2357,6 +2393,7 @@ impl ResponsePayload { Self::PermissionDecision(_) => "permission_decision", Self::PersistenceState(_) => "persistence_state", Self::PersistenceFlushed(_) => "persistence_flushed", + Self::PackageLinked(_) => "package_linked", Self::Rejected(_) => "rejected", Self::ExtResult(_) => "ext_result", } diff --git a/crates/sidecar/src/service.rs b/crates/sidecar/src/service.rs index aca756f83..5800a3997 100644 --- a/crates/sidecar/src/service.rs +++ b/crates/sidecar/src/service.rs @@ -1441,6 +1441,7 @@ where RequestPayload::Ext(payload) => { self.dispatch_extension_request(&request, payload).await } + RequestPayload::LinkPackage(payload) => self.link_package(&request, payload).await, }; match result { diff --git a/crates/sidecar/src/state.rs b/crates/sidecar/src/state.rs index 02622b7af..462568881 100644 --- a/crates/sidecar/src/state.rs +++ b/crates/sidecar/src/state.rs @@ -353,6 +353,10 @@ pub(crate) struct VmState { pub(crate) layers: VmLayerStore, pub(crate) command_guest_paths: BTreeMap, pub(crate) command_permissions: BTreeMap, + /// Host staging dir behind the `/opt/agentos` projection mount (built by + /// `configure_vm` from the wire `packages`); runtime `LinkPackage` appends to + /// it live. Owned by the sidecar; removed on dispose. + pub(crate) packages_staging_root: Option, pub(crate) toolkits: BTreeMap, pub(crate) active_processes: BTreeMap, pub(crate) exited_process_snapshots: VecDeque, diff --git a/crates/sidecar/src/stdio.rs b/crates/sidecar/src/stdio.rs index bb3cfe49d..7e1fd7bfd 100644 --- a/crates/sidecar/src/stdio.rs +++ b/crates/sidecar/src/stdio.rs @@ -521,7 +521,8 @@ fn extension_interrupt_response( | RequestPayload::GetZombieTimerCountRequest | RequestPayload::HostFilesystemCallRequest(_) | RequestPayload::PersistenceLoadRequest(_) - | RequestPayload::PersistenceFlushRequest(_) => return None, + | RequestPayload::PersistenceFlushRequest(_) + | RequestPayload::LinkPackageRequest(_) => return None, }; let interrupted_dispatch = interrupted_extension_dispatch( active_request, @@ -598,7 +599,8 @@ fn interrupted_extension_dispatch( | RequestPayload::GetZombieTimerCountRequest | RequestPayload::HostFilesystemCallRequest(_) | RequestPayload::PersistenceLoadRequest(_) - | RequestPayload::PersistenceFlushRequest(_) => { + | RequestPayload::PersistenceFlushRequest(_) + | RequestPayload::LinkPackageRequest(_) => { unreachable!("interrupted extension dispatch requires an extension request"); } } diff --git a/crates/sidecar/src/vm.rs b/crates/sidecar/src/vm.rs index 8c9527895..e6310cf6a 100644 --- a/crates/sidecar/src/vm.rs +++ b/crates/sidecar/src/vm.rs @@ -11,12 +11,12 @@ use crate::bridge::{bridge_permissions, MountPluginContext}; use crate::protocol::{ ConfigureVmRequest, CreateLayerRequest, CreateOverlayRequest, DisposeReason, EventFrame, ExportSnapshotRequest, ImportSnapshotRequest, LayerCreatedResponse, LayerSealedResponse, - MountDescriptor, MountPluginDescriptor, OverlayCreatedResponse, PermissionsPolicy, - ResponsePayload, RootFilesystemDescriptor, RootFilesystemEntry, RootFilesystemEntryEncoding, - RootFilesystemLowerDescriptor, RootFilesystemMode, RootFilesystemSnapshotResponse, - SealLayerRequest, SnapshotExportedResponse, SnapshotImportedResponse, - SnapshotRootFilesystemRequest, VmConfiguredResponse, VmCreatedResponse, VmDisposedResponse, - VmLifecycleState, + LinkPackageRequest, MountDescriptor, MountPluginDescriptor, OverlayCreatedResponse, + PackageLinkedResponse, PermissionsPolicy, ResponsePayload, RootFilesystemDescriptor, + RootFilesystemEntry, RootFilesystemEntryEncoding, RootFilesystemLowerDescriptor, + RootFilesystemMode, RootFilesystemSnapshotResponse, SealLayerRequest, SnapshotExportedResponse, + SnapshotImportedResponse, SnapshotRootFilesystemRequest, VmConfiguredResponse, + VmCreatedResponse, VmDisposedResponse, VmLifecycleState, }; use crate::service::{ audit_fields, dirname, emit_security_audit_event, emit_structured_event, kernel_error, @@ -103,7 +103,7 @@ const SHADOW_ROOT_BOOTSTRAP_DIRS: &[(&str, u32)] = &[ ]; pub(crate) const DEFAULT_GUEST_PATH_ENV: &str = - "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; + "/usr/local/sbin:/usr/local/bin:/opt/agentos/bin:/usr/sbin:/usr/bin:/sbin:/bin"; #[cfg(test)] const KERNEL_COMMAND_STUB: &[u8] = b"#!/bin/sh\n# kernel command stub\n"; pub(crate) const MAX_VM_LAYERS: usize = 256; @@ -164,6 +164,14 @@ where .set_vm_permissions(&vm_id, &permissions_policy)?; let permissions = bridge_permissions(self.bridge.clone(), &vm_id); let mut guest_env = filter_env(&vm_id, &create_config.env, &permissions); + // Ensure the kernel/guest sees the default `$PATH` (including `/opt/agentos/bin`) + // even when the client supplies no env (e.g. the Rust client sends an empty + // env): the per-VM command dirs are merged in below by + // `refresh_guest_command_path_env`, but `config.env` (the kernel's env, used + // by its `$PATH` walk) is cloned BEFORE that, so seed the default here. + guest_env + .entry(String::from("PATH")) + .or_insert_with(|| DEFAULT_GUEST_PATH_ENV.to_string()); // Sidecar-owned bootstrap work still needs to reconcile command stubs and the root // filesystem before the guest-visible policy takes effect. self.bridge @@ -283,6 +291,7 @@ where layers: VmLayerStore::default(), command_guest_paths, command_permissions: BTreeMap::new(), + packages_staging_root: None, toolkits: BTreeMap::new(), active_processes: BTreeMap::new(), exited_process_snapshots: VecDeque::new(), @@ -402,6 +411,18 @@ where bridge.set_vm_permissions(&vm_id, &PermissionsPolicy::allow_all())?; let mut effective_mounts = payload.mounts.clone(); append_module_access_mount(&mut effective_mounts, payload.module_access_cwd.as_ref())?; + // Build the `/opt/agentos` package projection in a sidecar-owned staging dir + // and register its read-only host_dir mount ourselves (the client forwards the + // package descriptors over the wire instead of staging them host-side). The + // mount is always created — even with no boot packages — so runtime + // `LinkPackage` can append to the live, host-backed staging dir. + if let Some(old) = vm.packages_staging_root.take() { + let _ = fs::remove_dir_all(&old); + } + let (packages_staging_root, packages_mount) = + build_packages_projection(&vm_id, &payload.packages, &payload.packages_mount_at)?; + vm.packages_staging_root = Some(packages_staging_root); + effective_mounts.push(packages_mount); let reconfigure_result = reconcile_mounts( mount_plugins, vm, @@ -477,6 +498,40 @@ where }) } + /// Runtime dynamic `linkSoftware`: project one package into the live + /// `/opt/agentos` staging dir. The host-dir mount reflects host writes, so the + /// package's `bin/` commands appear under `/opt/agentos/bin` (on `$PATH`) + /// immediately, with no reboot. Returns the linked command names. + pub(crate) async fn link_package( + &mut self, + request: &crate::protocol::RequestFrame, + payload: LinkPackageRequest, + ) -> Result { + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; + + let vm = self.vms.get_mut(&vm_id).expect("owned VM should exist"); + let staging_root = vm.packages_staging_root.clone().ok_or_else(|| { + SidecarError::InvalidState(String::from( + "link_package: VM has no /opt/agentos projection (configure_vm was not called)", + )) + })?; + let descriptor = crate::package_projection::PackageDescriptor { + name: payload.package.name.clone(), + dir: payload.package.dir.clone(), + acp_entrypoint: payload.package.acp_entrypoint.clone(), + }; + let commands = crate::package_projection::link_package(&descriptor, &staging_root)?; + + Ok(DispatchResult { + response: self.respond( + request, + ResponsePayload::PackageLinked(PackageLinkedResponse { commands }), + ), + events: Vec::new(), + }) + } + pub(crate) async fn create_layer( &mut self, request: &crate::protocol::RequestFrame, @@ -663,6 +718,9 @@ where // the output-buffer map was never reclaimed at all (M6). self.reclaim_vm_tracking(session_id, vm_id); let _ = fs::remove_dir_all(&vm.cwd); + if let Some(staging_root) = vm.packages_staging_root.take() { + let _ = fs::remove_dir_all(&staging_root); + } // Surface the first failure only AFTER cleanup has completed. terminate_result?; @@ -1296,6 +1354,52 @@ fn append_module_access_mount( Ok(()) } +/// Build the `/opt/agentos` package projection for `configure_vm`: create a +/// sidecar-owned staging dir, project each wire `PackageDescriptor` into it, and +/// return the staging path plus the read-only `host_dir` mount that exposes it in +/// the guest. Always returns a (possibly empty) projection so runtime `LinkPackage` +/// has a live host-backed dir to append to. +fn build_packages_projection( + vm_id: &str, + packages: &[crate::protocol::PackageDescriptor], + mount_at: &str, +) -> Result<(PathBuf, MountDescriptor), SidecarError> { + let mount_at = if mount_at.is_empty() { + crate::package_projection::OPT_AGENTOS_ROOT + } else { + mount_at + }; + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| { + SidecarError::Io(format!("failed to compute packages-staging nonce: {error}")) + })? + .as_nanos(); + let staging = std::env::temp_dir().join(format!("agentos-opt-{vm_id}-{nonce}")); + crate::package_projection::init_projection(&staging)?; + for pkg in packages { + let descriptor = crate::package_projection::PackageDescriptor { + name: pkg.name.clone(), + dir: pkg.dir.clone(), + acp_entrypoint: pkg.acp_entrypoint.clone(), + }; + crate::package_projection::link_package(&descriptor, &staging)?; + } + let mount = MountDescriptor { + guest_path: mount_at.to_string(), + read_only: true, + plugin: MountPluginDescriptor { + id: String::from("host_dir"), + config: serde_json::json!({ + "hostPath": staging, + "readOnly": true, + }) + .to_string(), + }, + }; + Ok((staging, mount)) +} + fn append_module_access_symlink_mounts( mounts: &mut Vec, node_modules_root: &Path, diff --git a/crates/sidecar/tests/architecture_guards.rs b/crates/sidecar/tests/architecture_guards.rs index dc84f68e8..f81b85a5f 100644 --- a/crates/sidecar/tests/architecture_guards.rs +++ b/crates/sidecar/tests/architecture_guards.rs @@ -259,6 +259,11 @@ const FS_ALLOW: &[&str] = &[ "crates/sidecar/src/stdio.rs", "crates/sidecar/src/state.rs", "crates/sidecar/src/vm.rs", + // package projection: materializes the read-only `/opt/agentos` package mount + // (symlink farm + shared, hardlinked content cache) from host package dirs. + // Sidecar-side, host-only, not guest-reachable -- same sanctioned boundary class + // as the host-dir/filesystem chokepoints. + "crates/sidecar/src/package_projection.rs", "crates/sidecar/src/service.rs", "crates/sidecar/src/execution.rs", "crates/sidecar/src/plugins/chunked_local.rs", diff --git a/crates/sidecar/tests/filesystem.rs b/crates/sidecar/tests/filesystem.rs index bc3ee7c30..cf7533470 100644 --- a/crates/sidecar/tests/filesystem.rs +++ b/crates/sidecar/tests/filesystem.rs @@ -416,6 +416,8 @@ mod shadow_root { projected_modules: Vec::new(), command_permissions: HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure command mount"); diff --git a/crates/sidecar/tests/generated_protocol.rs b/crates/sidecar/tests/generated_protocol.rs index fdad21b9c..3fd4ceabd 100644 --- a/crates/sidecar/tests/generated_protocol.rs +++ b/crates/sidecar/tests/generated_protocol.rs @@ -109,6 +109,8 @@ fn live_bare_codec_matches_generated_request_bytes() { live_protocol::WasmPermissionTier::ReadOnly, )]), loopback_exempt_ports: vec![3000], + packages: Vec::new(), + packages_mount_at: String::new(), }), )); let live_configure_payload = @@ -269,6 +271,8 @@ fn generated_configure_frame() -> ProtocolFrame { }], command_permissions: HashMap::from([("cat".to_string(), WasmPermissionTier::ReadOnly)]), loopback_exempt_ports: vec![3000], + packages: Vec::new(), + packages_mount_at: String::new(), }), }) } diff --git a/crates/sidecar/tests/guest_identity.rs b/crates/sidecar/tests/guest_identity.rs index 952a6ef15..dfdc1eec6 100644 --- a/crates/sidecar/tests/guest_identity.rs +++ b/crates/sidecar/tests/guest_identity.rs @@ -16,7 +16,8 @@ use support::{ temp_dir, wire_permissions_allow_all, wire_request, wire_session, }; -const DEFAULT_GUEST_PATH_ENV: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; +const DEFAULT_GUEST_PATH_ENV: &str = + "/usr/local/sbin:/usr/local/bin:/opt/agentos/bin:/usr/sbin:/usr/bin:/sbin:/bin"; const GUEST_IDENTITY_CASES: &[&str] = &["javascript", "python", "wasm_identity", "wasm_env"]; fn create_vm_with_root_filesystem( diff --git a/crates/sidecar/tests/layer_management.rs b/crates/sidecar/tests/layer_management.rs index 0382d9625..8c31e89a6 100644 --- a/crates/sidecar/tests/layer_management.rs +++ b/crates/sidecar/tests/layer_management.rs @@ -518,12 +518,15 @@ fn vm_layer_rpcs_and_module_access_mounts_are_scoped_per_vm() { projected_modules: Vec::new(), command_permissions: HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure vm"); match configure.response.payload { ResponsePayload::VmConfiguredResponse(response) => { - assert_eq!(response.applied_mounts, 1); + // 1 module-access mount + the always-on `/opt/agentos` package projection. + assert_eq!(response.applied_mounts, 2); } other => panic!("unexpected configure response: {other:?}"), } diff --git a/crates/sidecar/tests/node_modules_host_mount_resolution.rs b/crates/sidecar/tests/node_modules_host_mount_resolution.rs index e7b5abab4..66aeacc14 100644 --- a/crates/sidecar/tests/node_modules_host_mount_resolution.rs +++ b/crates/sidecar/tests/node_modules_host_mount_resolution.rs @@ -108,6 +108,8 @@ fn host_mounted_node_modules_package_resolves_from_guest_import() { projected_modules: Vec::new(), command_permissions: HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("mount host node_modules"); diff --git a/crates/sidecar/tests/node_modules_symlink_resolution.rs b/crates/sidecar/tests/node_modules_symlink_resolution.rs index 124218728..f31b85260 100644 --- a/crates/sidecar/tests/node_modules_symlink_resolution.rs +++ b/crates/sidecar/tests/node_modules_symlink_resolution.rs @@ -146,6 +146,8 @@ fn pnpm_symlinked_packages_resolve_from_guest_import() { projected_modules: Vec::new(), command_permissions: HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("mount host node_modules"); diff --git a/crates/sidecar/tests/package_projection.rs b/crates/sidecar/tests/package_projection.rs new file mode 100644 index 000000000..b421c4261 --- /dev/null +++ b/crates/sidecar/tests/package_projection.rs @@ -0,0 +1,275 @@ +//! Unit tests for the sidecar package projection (moved here from the agent-os +//! Rust client's `package_projection_test.rs`). These assert the on-disk +//! `/opt/agentos` layout, the shared-inode content cache, and version-keyed cache +//! invalidation directly against the projection module — no VM required. + +use std::fs; +use std::os::unix::fs::MetadataExt; +use std::path::{Path, PathBuf}; + +use secure_exec_sidecar::package_projection::{ + derive_commands, init_projection, link_package, read_package_version, PackageDescriptor, +}; + +fn unique_dir(tag: &str) -> PathBuf { + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!("agentos-projtest-{tag}-{nonce}")); + fs::create_dir_all(&dir).unwrap(); + dir +} + +/// Write a minimal toolchain-style package dir: root `package.json {name,version}` +/// plus a `bin/` with the given executable command files. +fn write_package(root: &Path, name: &str, version: &str, commands: &[&str]) { + fs::create_dir_all(root.join("bin")).unwrap(); + fs::write( + root.join("package.json"), + format!("{{\"name\":\"{name}\",\"version\":\"{version}\"}}"), + ) + .unwrap(); + for cmd in commands { + fs::write( + root.join("bin").join(cmd), + format!("#!/usr/bin/env node\n// {cmd}\n"), + ) + .unwrap(); + } +} + +#[test] +fn reads_version_from_package_json_and_errors_when_missing() { + let pkg = unique_dir("ver"); + write_package(&pkg, "vt", "3.1.4", &["vt"]); + assert_eq!( + read_package_version(pkg.to_str().unwrap()).unwrap(), + "3.1.4" + ); + + let empty = unique_dir("ver-missing"); + assert!(read_package_version(empty.to_str().unwrap()).is_err()); +} + +#[test] +fn derives_commands_from_bin_dir() { + let pkg = unique_dir("cmds"); + write_package(&pkg, "tool", "1.0.0", &["foo", "bar"]); + let mut commands = derive_commands(pkg.to_str().unwrap()).unwrap(); + commands.sort(); + assert_eq!(commands, vec!["bar".to_string(), "foo".to_string()]); +} + +#[test] +fn projects_bin_current_and_version_dir() { + let pkg = unique_dir("layout-src"); + write_package(&pkg, "demo", "2.0.0", &["demo"]); + + let staging = unique_dir("layout-staging"); + init_projection(&staging).unwrap(); + let commands = link_package( + &PackageDescriptor { + name: "demo".to_string(), + dir: pkg.to_string_lossy().into_owned(), + acp_entrypoint: None, + }, + &staging, + ) + .unwrap(); + assert_eq!(commands, vec!["demo".to_string()]); + + // bin/ is a relative in-tree symlink through current. + let bin_link = staging.join("bin").join("demo"); + assert_eq!( + fs::read_link(&bin_link).unwrap(), + Path::new("../demo/current/bin/demo"), + ); + // /current -> + assert_eq!( + fs::read_link(staging.join("demo").join("current")).unwrap(), + Path::new("2.0.0"), + ); + // package content lives under // + assert!(staging + .join("demo") + .join("2.0.0") + .join("bin") + .join("demo") + .exists()); +} + +#[test] +fn reprojecting_same_package_is_idempotent() { + // Two meta-packages can both pull in the same sub-package (e.g. build-essential AND + // common both include coreutils). Projecting the same name@version twice into one + // staging dir must be a no-op, NOT an EEXIST "symlink copy" conflict. + let pkg = unique_dir("idem-src"); + write_package(&pkg, "coreutils", "9.5.0", &["ls", "cat"]); + let desc = PackageDescriptor { + name: "coreutils".to_string(), + dir: pkg.to_string_lossy().into_owned(), + acp_entrypoint: None, + }; + let staging = unique_dir("idem-staging"); + init_projection(&staging).unwrap(); + + let first = link_package(&desc, &staging).unwrap(); + let second = link_package(&desc, &staging).unwrap(); // must not error + assert_eq!(first, second); + assert!(staging.join("bin").join("ls").exists()); + assert!(staging + .join("coreutils") + .join("9.5.0") + .join("bin") + .join("ls") + .exists()); +} + +#[test] +fn projects_from_package_json_bin_without_bin_symlinks() { + // An npm-shippable package: commands declared in package.json "bin" pointing at + // real entry files; NO bin/ symlink dir in the package. + let pkg = unique_dir("pjbin-src"); + fs::create_dir_all(pkg.join("dist")).unwrap(); + fs::write( + pkg.join("package.json"), + "{\"name\":\"@scope/tool\",\"version\":\"4.5.6\",\"bin\":{\"mytool\":\"./dist/cli.js\"}}", + ) + .unwrap(); + fs::write(pkg.join("dist/cli.js"), "#!/usr/bin/env node\n").unwrap(); + + let staging = unique_dir("pjbin-staging"); + init_projection(&staging).unwrap(); + let commands = link_package( + &PackageDescriptor { + name: "@scope/tool".to_string(), + dir: pkg.to_string_lossy().into_owned(), + acp_entrypoint: Some("mytool".to_string()), + }, + &staging, + ) + .unwrap(); + assert_eq!(commands, vec!["mytool".to_string()]); + // /opt/agentos/bin/mytool -> ..//current/dist/cli.js (the real entry). + assert_eq!( + fs::read_link(staging.join("bin").join("mytool")).unwrap(), + Path::new("../@scope/tool/current/dist/cli.js"), + ); +} + +#[test] +fn hardlinks_content_so_two_projections_share_inodes() { + let pkg = unique_dir("share-src"); + write_package(&pkg, "shared", "9.9.9", &["shared"]); + let desc = PackageDescriptor { + name: "shared".to_string(), + dir: pkg.to_string_lossy().into_owned(), + acp_entrypoint: None, + }; + + let staging_a = unique_dir("share-a"); + let staging_b = unique_dir("share-b"); + init_projection(&staging_a).unwrap(); + init_projection(&staging_b).unwrap(); + link_package(&desc, &staging_a).unwrap(); + link_package(&desc, &staging_b).unwrap(); + + let a = fs::metadata(staging_a.join("shared/9.9.9/bin/shared")).unwrap(); + let b = fs::metadata(staging_b.join("shared/9.9.9/bin/shared")).unwrap(); + assert_eq!( + a.ino(), + b.ino(), + "cross-projection content should share an inode" + ); + assert!(a.nlink() >= 2, "hardlinked content should have nlink >= 2"); +} + +#[test] +fn invalidates_cache_on_version_change() { + let pkg_v1 = unique_dir("inval-v1"); + write_package(&pkg_v1, "p", "1.0.0", &["p"]); + let pkg_v2 = unique_dir("inval-v2"); + write_package(&pkg_v2, "p", "2.0.0", &["p"]); + + let staging_a = unique_dir("inval-a"); + let staging_b = unique_dir("inval-b"); + init_projection(&staging_a).unwrap(); + init_projection(&staging_b).unwrap(); + link_package( + &PackageDescriptor { + name: "p".to_string(), + dir: pkg_v1.to_string_lossy().into_owned(), + acp_entrypoint: None, + }, + &staging_a, + ) + .unwrap(); + link_package( + &PackageDescriptor { + name: "p".to_string(), + dir: pkg_v2.to_string_lossy().into_owned(), + acp_entrypoint: None, + }, + &staging_b, + ) + .unwrap(); + + let a = fs::metadata(staging_a.join("p/1.0.0/bin/p")).unwrap(); + let b = fs::metadata(staging_b.join("p/2.0.0/bin/p")).unwrap(); + assert_ne!( + a.ino(), + b.ino(), + "a version bump must produce a fresh cache entry" + ); +} + +#[test] +fn rejects_duplicate_command_across_packages() { + let pkg_a = unique_dir("dup-a"); + write_package(&pkg_a, "a", "1.0.0", &["clash"]); + let pkg_b = unique_dir("dup-b"); + write_package(&pkg_b, "b", "1.0.0", &["clash"]); + + let staging = unique_dir("dup-staging"); + init_projection(&staging).unwrap(); + link_package( + &PackageDescriptor { + name: "a".to_string(), + dir: pkg_a.to_string_lossy().into_owned(), + acp_entrypoint: None, + }, + &staging, + ) + .unwrap(); + let err = link_package( + &PackageDescriptor { + name: "b".to_string(), + dir: pkg_b.to_string_lossy().into_owned(), + acp_entrypoint: None, + }, + &staging, + ); + assert!(err.is_err(), "duplicate command must be rejected"); +} + +#[test] +fn rejects_unknown_acp_entrypoint() { + let pkg = unique_dir("acp-src"); + write_package(&pkg, "agentpkg", "1.0.0", &["real-cmd"]); + + let staging = unique_dir("acp-staging"); + init_projection(&staging).unwrap(); + let err = link_package( + &PackageDescriptor { + name: "agentpkg".to_string(), + dir: pkg.to_string_lossy().into_owned(), + acp_entrypoint: Some("does-not-exist".to_string()), + }, + &staging, + ); + assert!( + err.is_err(), + "acpEntrypoint not in commands must be rejected" + ); +} diff --git a/crates/sidecar/tests/permission_flags.rs b/crates/sidecar/tests/permission_flags.rs index c4dbe92cb..2b6923f12 100644 --- a/crates/sidecar/tests/permission_flags.rs +++ b/crates/sidecar/tests/permission_flags.rs @@ -218,6 +218,8 @@ fn permission_flags_reject_empty_paths_and_patterns_on_configure() { projected_modules: Vec::new(), command_permissions: Default::default(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("dispatch configure vm with empty fs paths"); @@ -256,6 +258,8 @@ fn permission_flags_reject_empty_paths_and_patterns_on_configure() { projected_modules: Vec::new(), command_permissions: Default::default(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("dispatch configure vm with empty network patterns"); @@ -294,6 +298,8 @@ fn permission_flags_reject_empty_paths_and_patterns_on_configure() { projected_modules: Vec::new(), command_permissions: Default::default(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("dispatch configure vm with empty network operations"); diff --git a/crates/sidecar/tests/posix_path_repro.rs b/crates/sidecar/tests/posix_path_repro.rs index e081447f6..8db6d567b 100644 --- a/crates/sidecar/tests/posix_path_repro.rs +++ b/crates/sidecar/tests/posix_path_repro.rs @@ -99,6 +99,8 @@ fn configure_mounts( projected_modules: Vec::new(), command_permissions: HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure registry command mount"); diff --git a/crates/sidecar/tests/protocol.rs b/crates/sidecar/tests/protocol.rs index 448edfe6e..dce64c854 100644 --- a/crates/sidecar/tests/protocol.rs +++ b/crates/sidecar/tests/protocol.rs @@ -877,6 +877,8 @@ fn schema_supports_configuration_and_structured_events() { }], command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )); diff --git a/crates/sidecar/tests/python.rs b/crates/sidecar/tests/python.rs index befa4561b..59a6833b4 100644 --- a/crates/sidecar/tests/python.rs +++ b/crates/sidecar/tests/python.rs @@ -1016,6 +1016,61 @@ fn python_runtime_executes_workspace_py_file_by_path() { ); } +/// Phase 2 file-route: a `#!…python3` script run as a COMMAND (no runtime hint) +/// must be dispatched to the Python runtime by its header, symmetric with a +/// `#!…node` script dispatching to JS. Proves binfmt header dispatch, not a +/// runtime hint, selects Pyodide. +fn python_runtime_executes_shebang_file_via_command_path() { + assert_node_available(); + + let mut sidecar = new_sidecar("python-shebang-cmd"); + let cwd = temp_dir("python-shebang-cmd-cwd"); + let connection_id = authenticate_wire(&mut sidecar, "conn-python"); + let session_id = open_session_wire(&mut sidecar, 2, &connection_id); + let mut script_entry = root_file( + "/workspace/pytool", + "#!/usr/bin/env python3\nprint('hello from shebang')\n", + Some(RootFilesystemEntryEncoding::Utf8), + ); + script_entry.executable = true; + let vm_id = create_vm_with_root_filesystem( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::Python, + &cwd, + RootFilesystemDescriptor { + mode: RootFilesystemMode::Ephemeral, + disable_default_base_layer: false, + lowers: Vec::new(), + bootstrap_entries: vec![root_dir("/workspace"), script_entry], + }, + ); + + execute_python_cli( + &mut sidecar, + 4, + &connection_id, + &session_id, + &vm_id, + "proc-py-shebang", + "/workspace/pytool", + &[], + ); + + let (stdout, stderr, exit_code) = collect_process_output( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + "proc-py-shebang", + ); + + assert_eq!(exit_code, 0, "stdout: {stdout}\nstderr: {stderr}"); + assert_eq!(stdout, "hello from shebang\n"); +} + fn python_runtime_reports_syntax_errors_over_stderr() { assert_node_available(); @@ -1795,6 +1850,8 @@ if (mode === 'write') { projected_modules: Vec::new(), command_permissions: HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure host_dir workspace mount through wire"); @@ -3661,6 +3718,8 @@ process.stdout.write('status=' + result.status + ';out=' + (result.stdout || '') projected_modules: Vec::new(), command_permissions: HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure host_dir workspace mount through wire"); @@ -3880,6 +3939,7 @@ fn python_suite() { static_file_server_rejects_traversal_paths(); python_runtime_executes_code_end_to_end(); python_runtime_executes_workspace_py_file_by_path(); + python_runtime_executes_shebang_file_via_command_path(); python_runtime_reports_syntax_errors_over_stderr(); python_runtime_blocks_pyodide_js_escape_hatches(); concurrent_python_processes_stay_isolated_across_vms(); diff --git a/crates/sidecar/tests/security_audit.rs b/crates/sidecar/tests/security_audit.rs index c4f1d754c..3fbd01b06 100644 --- a/crates/sidecar/tests/security_audit.rs +++ b/crates/sidecar/tests/security_audit.rs @@ -124,6 +124,8 @@ fn filesystem_permission_denials_emit_security_audit_events() { projected_modules: Vec::new(), command_permissions: HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure vm permissions"); @@ -252,6 +254,8 @@ fn mount_operations_emit_security_audit_events() { projected_modules: Vec::new(), command_permissions: HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("mount workspace"); @@ -269,6 +273,8 @@ fn mount_operations_emit_security_audit_events() { projected_modules: Vec::new(), command_permissions: HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("unmount workspace"); @@ -281,10 +287,15 @@ fn mount_operations_emit_security_audit_events() { assert_eq!(mounted.fields["read_only"], "false"); assert_timestamp(mounted); + // The always-on `/opt/agentos` package projection also churns unmount events on + // reconfigure, so select the workspace unmount explicitly rather than the last one. let unmounted = events .iter() - .rfind(|event| event.name == "security.mount.unmounted") - .expect("missing unmount audit event"); + .rfind(|event| { + event.name == "security.mount.unmounted" + && event.fields.get("guest_path").map(String::as_str) == Some("/workspace") + }) + .expect("missing unmount audit event for /workspace"); assert_eq!(unmounted.vm_id, vm_id); assert_eq!(unmounted.fields["guest_path"], "/workspace"); assert_eq!(unmounted.fields["plugin_id"], "memory"); diff --git a/crates/sidecar/tests/security_hardening.rs b/crates/sidecar/tests/security_hardening.rs index f408cb2a1..b8caf50bd 100644 --- a/crates/sidecar/tests/security_hardening.rs +++ b/crates/sidecar/tests/security_hardening.rs @@ -22,7 +22,8 @@ use support::{ const ARG_PREFIX: &str = "ARG="; const INVOCATION_BREAK: &str = "--END--"; -const DEFAULT_GUEST_PATH_ENV: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; +const DEFAULT_GUEST_PATH_ENV: &str = + "/usr/local/sbin:/usr/local/bin:/opt/agentos/bin:/usr/sbin:/usr/bin:/sbin:/bin"; const DEFAULT_GUEST_HOME: &str = "/home/agentos"; const MAX_SECURITY_HARDENING_STREAM_BYTES: usize = 1024 * 1024; struct EnvVarGuard { @@ -494,6 +495,8 @@ fn execute_rejects_host_only_absolute_command_path() { projected_modules: Vec::new(), command_permissions: HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure host-only command permissions"); diff --git a/crates/sidecar/tests/service.rs b/crates/sidecar/tests/service.rs index cd1bfd1c9..2e96caaa9 100644 --- a/crates/sidecar/tests/service.rs +++ b/crates/sidecar/tests/service.rs @@ -37,6 +37,9 @@ mod macos_fs; #[path = "../src/metadata/mod.rs"] mod metadata; #[allow(dead_code)] +#[path = "../src/package_projection.rs"] +mod package_projection; +#[allow(dead_code)] #[path = "../src/plugins/mod.rs"] mod plugins; #[allow(dead_code, clippy::enum_variant_names)] @@ -5959,6 +5962,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure mounts"); @@ -5985,6 +5990,13 @@ ykAheWCsAteSEWVc0w==\n\ assert_eq!( vm.kernel.mounted_filesystems(), vec![ + // Always-on package projection mount built by configure_vm + // (see vm.rs build_packages_projection); present even with no packages. + MountEntry { + path: String::from("/opt/agentos"), + plugin_id: String::from("host_dir"), + read_only: true, + }, MountEntry { path: String::from("/workspace"), plugin_id: String::from("memory"), @@ -6030,6 +6042,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure readonly mount"); @@ -6103,6 +6117,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure host_dir mount"); @@ -6174,6 +6190,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure host_dir mount"); @@ -6229,6 +6247,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure module_access mount"); @@ -6452,6 +6472,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure module_access mount"); @@ -6517,6 +6539,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure js_bridge mount"); @@ -6650,6 +6674,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure js_bridge mount"); @@ -6736,6 +6762,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure js_bridge mount"); @@ -6851,6 +6879,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure js_bridge mount"); @@ -6945,6 +6975,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure sandbox_agent mount"); @@ -7048,6 +7080,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure s3 mount"); @@ -7143,6 +7177,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure object_s3 mount"); @@ -7222,6 +7258,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure chunked_local mount"); @@ -7623,6 +7661,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("dispatch configure_vm failure"); @@ -7840,6 +7880,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("dispatch fs configure vm"); @@ -7886,6 +7928,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("dispatch network configure vm"); @@ -7942,13 +7986,16 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("dispatch configure vm"); match result.response.payload { ResponsePayload::VmConfigured(response) => { - assert_eq!(response.applied_mounts, 1); + // 1 client mount + the always-on /opt/agentos package projection mount. + assert_eq!(response.applied_mounts, 2); } other => panic!("expected configured response, got {other:?}"), } @@ -8141,13 +8188,16 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("dispatch configure vm"); match result.response.payload { ResponsePayload::VmConfigured(response) => { - assert_eq!(response.applied_mounts, 1); + // 1 client mount + the always-on /opt/agentos package projection mount. + assert_eq!(response.applied_mounts, 2); } other => panic!("expected configured response, got {other:?}"), } @@ -8204,13 +8254,16 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure operator mount"); match configure_response.response.payload { ResponsePayload::VmConfigured(configured) => { - assert_eq!(configured.applied_mounts, 1); + // 1 client mount + the always-on /opt/agentos package projection mount. + assert_eq!(configured.applied_mounts, 2); } other => panic!("expected configured response, got {other:?}"), } @@ -8221,7 +8274,11 @@ ykAheWCsAteSEWVc0w==\n\ .expect("configured vm") .kernel .mounted_filesystems(); - assert_eq!(operator_mounts.len(), 2, "root + operator-applied mount"); + assert_eq!( + operator_mounts.len(), + 3, + "root + operator-applied mount + always-on /opt/agentos package projection" + ); let mount_error = sidecar .vms @@ -8355,6 +8412,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure host_dir mount"); @@ -8498,6 +8557,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure command mount"); @@ -8588,6 +8649,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure command mount"); @@ -8851,6 +8914,8 @@ ykAheWCsAteSEWVc0w==\n\ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure command-path mounts"); @@ -9888,6 +9953,8 @@ process.stdout.write(`${JSON.stringify({ projected_modules: Vec::new(), command_permissions: std::collections::HashMap::new(), loopback_exempt_ports: vec![4312], + packages: Vec::new(), + packages_mount_at: String::new(), }), )) .expect("configure workspace mount"); diff --git a/crates/sidecar/tests/stdio_binary.rs b/crates/sidecar/tests/stdio_binary.rs index 7dbbf0e46..7c427ea0e 100644 --- a/crates/sidecar/tests/stdio_binary.rs +++ b/crates/sidecar/tests/stdio_binary.rs @@ -846,13 +846,16 @@ fn native_sidecar_binary_supports_js_bridge_host_filesystem_access() { projected_modules: Vec::new(), command_permissions: HashMap::new(), loopback_exempt_ports: Vec::new(), + packages: Vec::new(), + packages_mount_at: String::new(), }), ), ); let configured = recv_response(&mut stdout, &codec, 4, &mut buffered_events); match configured.payload { ResponsePayload::VmConfiguredResponse(response) => { - assert_eq!(response.applied_mounts, 1); + // 1 user mount + the always-on `/opt/agentos` package projection. + assert_eq!(response.applied_mounts, 2); assert_eq!(response.applied_software, 0); } other => panic!("unexpected configure response: {other:?}"), diff --git a/crates/vfs/assets/base-filesystem.json b/crates/vfs/assets/base-filesystem.json index 64e03bfa5..e56e06407 100644 --- a/crates/vfs/assets/base-filesystem.json +++ b/crates/vfs/assets/base-filesystem.json @@ -1,6 +1,5 @@ { "source": { - "snapshotPath": "alpine-defaults.json", "image": "alpine:3.22", "snapshotCreatedAt": "2026-06-22T19:45:20.529Z", "builtAt": "2026-06-23T03:47:12.644Z", @@ -19,7 +18,8 @@ "LC_COLLATE": "C", "LOGNAME": "agentos", "PAGER": "less", - "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "PATH": "/usr/local/sbin:/usr/local/bin:/opt/agentos/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "MANPATH": "/opt/agentos/share/man:/usr/local/share/man:/usr/share/man", "SHELL": "/bin/sh", "USER": "agentos" }, @@ -165,7 +165,7 @@ "mode": "644", "uid": 0, "gid": 0, - "content": "export PATH=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n\nexport PAGER=less\numask 022\n\n# use nicer PS1 for bash and busybox ash\nif [ -n \"$BASH_VERSION\" -o \"$BB_ASH_VERSION\" ]; then\n\tPS1='\\h:\\w\\$ '\n# use nicer PS1 for zsh\nelif [ -n \"$ZSH_VERSION\" ]; then\n\tPS1='%m:%~%# '\n# set up fallback default PS1\nelse\n\t: \"${HOSTNAME:=$(hostname)}\"\n\tPS1='${HOSTNAME%%.*}:$PWD'\n\t[ \"$(id -u)\" -eq 0 ] && PS1=\"${PS1}# \" || PS1=\"${PS1}\\$ \"\nfi\n\nfor script in /etc/profile.d/*.sh ; do\n\tif [ -r \"$script\" ] ; then\n\t\t. \"$script\"\n\tfi\ndone\nunset script\n" + "content": "export PATH=\"/usr/local/sbin:/usr/local/bin:/opt/agentos/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n\nexport PAGER=less\numask 022\n\n# use nicer PS1 for bash and busybox ash\nif [ -n \"$BASH_VERSION\" -o \"$BB_ASH_VERSION\" ]; then\n\tPS1='\\h:\\w\\$ '\n# use nicer PS1 for zsh\nelif [ -n \"$ZSH_VERSION\" ]; then\n\tPS1='%m:%~%# '\n# set up fallback default PS1\nelse\n\t: \"${HOSTNAME:=$(hostname)}\"\n\tPS1='${HOSTNAME%%.*}:$PWD'\n\t[ \"$(id -u)\" -eq 0 ] && PS1=\"${PS1}# \" || PS1=\"${PS1}\\$ \"\nfi\n\nfor script in /etc/profile.d/*.sh ; do\n\tif [ -r \"$script\" ] ; then\n\t\t. \"$script\"\n\tfi\ndone\nunset script\n" }, { "path": "/etc/profile.d", diff --git a/crates/vfs/build.rs b/crates/vfs/build.rs deleted file mode 100644 index 955b501ed..000000000 --- a/crates/vfs/build.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::{env, fs, path::PathBuf}; - -// Stage the base filesystem fixture into OUT_DIR. In-tree builds use the -// canonical secure-exec package fixture from the current workspace; the -// published crate falls back to the vendored `assets/base-filesystem.json` copy. -fn main() { - let manifest_dir = - PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set")); - let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR must be set")); - - println!("cargo:rerun-if-changed=build.rs"); - - let workspace_fixtures = [ - manifest_dir.join("../../packages/secure-exec-core/fixtures/base-filesystem.json"), - manifest_dir.join("../../packages/core/fixtures/base-filesystem.json"), - ]; - let vendored = manifest_dir.join("assets/base-filesystem.json"); - let src = workspace_fixtures - .into_iter() - .find(|fixture| fixture.exists()) - .unwrap_or(vendored); - - println!("cargo:rerun-if-changed={}", src.display()); - - let dest = out_dir.join("base-filesystem.json"); - fs::copy(&src, &dest).unwrap_or_else(|error| { - panic!( - "failed to stage base-filesystem.json from {} to {}: {}", - src.display(), - dest.display(), - error - ) - }); -} diff --git a/crates/vfs/src/posix/root_fs.rs b/crates/vfs/src/posix/root_fs.rs index b96b316be..4b9b7a9b0 100644 --- a/crates/vfs/src/posix/root_fs.rs +++ b/crates/vfs/src/posix/root_fs.rs @@ -11,12 +11,19 @@ use base64::Engine; use serde::Deserialize; use std::collections::BTreeSet; -// The base filesystem fixture is staged into OUT_DIR by build.rs: copied from -// the canonical `packages/secure-exec-core/fixtures/base-filesystem.json` -// during in-tree builds, or from the vendored `assets/base-filesystem.json` -// copy bundled in the published crate. -const BUNDLED_BASE_FILESYSTEM_JSON: &str = - include_str!(concat!(env!("OUT_DIR"), "/base-filesystem.json")); +// The single, manually-maintained base filesystem snapshot, embedded directly at +// compile time. This crate is the ONE owner of these bytes; other crates (e.g. +// the sidecar) read them via `base_filesystem_json()` rather than bundling a copy. +// Regenerate `assets/base-filesystem.json` by hand with the build-tools script; +// nothing builds it on the fly. +const BUNDLED_BASE_FILESYSTEM_JSON: &str = include_str!("../../assets/base-filesystem.json"); + +/// The bundled secure-exec base filesystem snapshot JSON, embedded at compile +/// time. The single Rust-side source of these bytes — depend on this crate +/// instead of vendoring another copy. +pub const fn base_filesystem_json() -> &'static str { + BUNDLED_BASE_FILESYSTEM_JSON +} pub const ROOT_FILESYSTEM_SNAPSHOT_FORMAT: &str = "secure_exec_filesystem_snapshot_v1"; const LEGACY_AGENTOS_ROOT_FILESYSTEM_SNAPSHOT_FORMAT: &str = "agentos_filesystem_snapshot_v1"; const ROOT_FILESYSTEM_SNAPSHOT_FIXED_OVERHEAD_BYTES: usize = 4 * 1024; diff --git a/packages/agentos-toolchain/README.md b/packages/agentos-toolchain/README.md new file mode 100644 index 000000000..72ce0f34c --- /dev/null +++ b/packages/agentos-toolchain/README.md @@ -0,0 +1,76 @@ +# @rivet-dev/agentos-toolchain + +Build toolchain for **agentOS packages** — the only sanctioned way to turn an npm +package or a local script into a valid, self-contained agentOS package. + +```bash +npx @rivet-dev/agentos-toolchain pack [options] +``` + +## Why this lives in secure-exec + +**Packaging is part of secure-exec's core behavior, so the tool that produces +packages lives here, next to the things it packages.** secure-exec owns the VM +runtime that *runs* packages — the kernel, the VFS, the `/opt/agentos` mount, the +`$PATH` command resolver, and the header/`binfmt` dispatch — and it owns the +package **definitions** themselves: the generic registry software (`registry/software/*`) +and the agent adapters (`registry/agent/*`). This toolchain is what builds those +definitions into the on-disk package format that the runtime resolves. Its +`header.ts` is the same `binfmt` table the sidecar enforces (`crates/sidecar`), so +keeping it in this repo keeps the producer and the consumer of the format in one +place. + +agent-os (the product layer) only *consumes* finished packages via +`defineSoftware({ name, dir })`; it does not need to own the builder. The package +name stays `@rivet-dev/agentos-toolchain` so the documented `npx` entrypoint is +unchanged. + +## `pack` + +Produces `///` — a package in the agentOS +[package format](https://agentos-sdk.dev/docs/architecture/packages-and-command-resolution): + +The output is a **flat, self-contained package directory** — a plain npm dependency, no +agentOS-specific manifest and no symlinks: + +``` +/ # the package dir itself (default ./-package) +├── package.json # name, version, and the "bin" command map +└── node_modules/ # flat, self-contained dependency closure +``` + +Commands are declared in `package.json` `"bin"` (command → a **real entry file**), so the +package ships cleanly via npm. The runtime builds the `/opt/agentos/bin` symlinks itself when +it mounts the package — they are never part of the shipped artifact. + +### Steps + +1. **Isolate** — a clean temp dir (no host pnpm/workspace bleed-through). +2. **Install flat** — `npm install --omit=dev` (full closure, hoisted, no scripts). +3. **Ensure `package.json`** — `name`/`version` from the package plus a `"bin"` map (each entry + gets a `#!` shebang). No `agentos-package.json` — `package.json` is the only metadata. +4. **Lay out flat** — `package.json` + `node_modules` written directly into `--out`. +5. **Verify** — every `bin` entry has a recognized header (`#!` shebang or `\0asm`); **reject** + native `.node` addons (they can't run in V8) — the error names `--prune-native` as the escape. + +### Options + +| Flag | Meaning | +|---|---| +| `--agent ` | mark a `bin` command as the package's ACP entrypoint | +| `--out ` | output dir for the package itself (flat; default `./-package`) | +| `--prune-native` | delete unreachable native `.node` addons from the flat closure instead of failing | + +### Examples + +```bash +# package a local CLI → ./my-tool-package/ +npx @rivet-dev/agentos-toolchain pack ./my-tool + +# an agent whose SDK closure carries unreachable native addons → ./pi-package/ +npx @rivet-dev/agentos-toolchain pack @agentos-software/pi --agent pi-sdk-acp --prune-native +``` + +The package is consumed by an agentOS host as `defineSoftware({ name, dir })` — add +`agent: { acpEntrypoint: "" }` for an agent. The host projects it under `/opt/agentos`, +and `createSession(name)` launches the adapter via `/opt/agentos/bin/`. diff --git a/packages/agentos-toolchain/package.json b/packages/agentos-toolchain/package.json new file mode 100644 index 000000000..3c7b37f8c --- /dev/null +++ b/packages/agentos-toolchain/package.json @@ -0,0 +1,34 @@ +{ + "name": "@rivet-dev/agentos-toolchain", + "version": "0.0.0", + "description": "Build toolchain for agentOS packages (pack npm packages/scripts into self-contained agentOS packages).", + "type": "module", + "license": "Apache-2.0", + "bin": { + "agentos-toolchain": "./dist/cli.js" + }, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist", + "package.json", + "README.md" + ], + "scripts": { + "build": "tsup src/index.ts src/cli.ts --format esm --dts", + "check-types": "tsc --noEmit", + "test": "vitest run" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsup": "^8.3.5", + "typescript": "^5.7.2", + "vitest": "^2.1.9" + } +} diff --git a/packages/agentos-toolchain/src/cli.ts b/packages/agentos-toolchain/src/cli.ts new file mode 100644 index 000000000..9d1ee71b1 --- /dev/null +++ b/packages/agentos-toolchain/src/cli.ts @@ -0,0 +1,84 @@ +#!/usr/bin/env node +import { existsSync, statSync } from "node:fs"; +import { basename, resolve } from "node:path"; +import { pack } from "./pack.js"; + +const USAGE = `agentos-toolchain — build agentOS packages + +Usage: + agentos-toolchain pack [options] + +Options: + --agent mark a bin command as the package's ACP entrypoint + --out output dir for the package itself (FLAT; + default: ./-package) + --prune-native delete unreachable native .node addons from the flat closure + -h, --help show this help +`; + +/** Default flat output dir: ./-package in cwd. */ +function defaultOutName(source: string): string { + if (existsSync(source) && statSync(source).isDirectory()) { + return `./${basename(resolve(source))}-package`; + } + // npm spec: strip a trailing @version, then the @scope/ prefix. + const at = source.lastIndexOf("@"); + const name = at > 0 ? source.slice(0, at) : source; + return `./${name.replace(/^@[^/]+\//, "")}-package`; +} + +function parseArgs(argv: string[]): { + cmd: string; + source?: string; + agent?: string; + out?: string; + pruneNative: boolean; +} { + const [cmd, ...rest] = argv; + let source: string | undefined; + let agent: string | undefined; + let out: string | undefined; + let pruneNative = false; + for (let i = 0; i < rest.length; i++) { + const a = rest[i]; + if (a === "--agent") agent = rest[++i]; + else if (a === "--out") out = rest[++i]; + else if (a === "--prune-native") pruneNative = true; + else if (a === "-h" || a === "--help") { + process.stdout.write(USAGE); + process.exit(0); + } else if (!a.startsWith("-") && source === undefined) source = a; + else throw new Error(`unexpected argument: ${a}`); + } + return { cmd, source, agent, out, pruneNative }; +} + +function main(): void { + const args = parseArgs(process.argv.slice(2)); + if (args.cmd === undefined || args.cmd === "-h" || args.cmd === "--help") { + process.stdout.write(USAGE); + process.exit(args.cmd === undefined ? 1 : 0); + } + if (args.cmd !== "pack") { + throw new Error(`unknown command "${args.cmd}" (only "pack" is supported)`); + } + if (!args.source) throw new Error("pack requires a argument"); + + const result = pack({ + source: args.source, + out: resolve(args.out ?? defaultOutName(args.source)), + agent: args.agent, + pruneNative: args.pruneNative, + }); + process.stdout.write( + `packed ${result.name}@${result.version} → ${result.packageDir}\n` + + ` commands: ${result.commands.join(", ")}\n`, + ); +} + +try { + main(); +} catch (error) { + process.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\n`); + process.exit(1); +} diff --git a/packages/agentos-toolchain/src/header.ts b/packages/agentos-toolchain/src/header.ts new file mode 100644 index 000000000..8caa99689 --- /dev/null +++ b/packages/agentos-toolchain/src/header.ts @@ -0,0 +1,64 @@ +/** + * Executable header detection — the same binfmt rules the runtime uses. + * + * Runtime is decided by the file's leading bytes, never its name or extension: + * a `#!` shebang, the `\0asm` WebAssembly magic, or a native object-file magic + * (ELF / Mach-O / PE) which agentOS cannot execute (no native-arch handler). + */ + +export type ExecutableKind = + | "shebang" + | "wasm" + | "native-elf" + | "native-macho" + | "native-pe" + | "unknown"; + +const WASM_MAGIC = Buffer.from([0x00, 0x61, 0x73, 0x6d]); // "\0asm" +const ELF_MAGIC = Buffer.from([0x7f, 0x45, 0x4c, 0x46]); // "\x7fELF" + +// Mach-O thin (LE/BE, 32/64) + fat/universal magics. +const MACHO_MAGICS = [ + 0xfeedface, 0xcefaedfe, 0xfeedfacf, 0xcffaedfe, 0xcafebabe, 0xbebafeca, +]; + +export function detectExecutableKind(head: Buffer): ExecutableKind { + if (head.length >= 2 && head[0] === 0x23 && head[1] === 0x21) { + return "shebang"; // "#!" + } + if (head.length >= 4 && head.subarray(0, 4).equals(WASM_MAGIC)) { + return "wasm"; + } + if (head.length >= 4 && head.subarray(0, 4).equals(ELF_MAGIC)) { + return "native-elf"; + } + if (head.length >= 4) { + const be = head.readUInt32BE(0); + // `cafebabe` collides with Java `.class`; disambiguate via the next field + // (Mach-O fat has a small arch count; a class file's major version is >= 45). + if (be === 0xcafebabe && head.length >= 8) { + const next = head.readUInt32BE(4); + if (next >= 45) return "unknown"; // Java class, not Mach-O + } + if (MACHO_MAGICS.includes(be)) return "native-macho"; + } + // PE: "MZ" with a PE header pointer; "MZ" alone is enough to flag as native. + if (head.length >= 2 && head[0] === 0x4d && head[1] === 0x5a) { + return "native-pe"; // "MZ" + } + return "unknown"; +} + +export function isNativeKind(kind: ExecutableKind): boolean { + return kind === "native-elf" || kind === "native-macho" || kind === "native-pe"; +} + +/** The interpreter named on a shebang line, taken literally (no PATH search). */ +export function parseShebangInterpreter(head: Buffer): string | null { + if (!(head.length >= 2 && head[0] === 0x23 && head[1] === 0x21)) return null; + const nl = head.indexOf(0x0a); + const line = head.subarray(2, nl < 0 ? head.length : nl).toString("utf8"); + const trimmed = line.replace(/^\s+/, ""); + const interp = trimmed.split(/\s+/, 1)[0] ?? ""; + return interp.length > 0 ? interp : null; +} diff --git a/packages/agentos-toolchain/src/index.ts b/packages/agentos-toolchain/src/index.ts new file mode 100644 index 000000000..949136bfe --- /dev/null +++ b/packages/agentos-toolchain/src/index.ts @@ -0,0 +1,7 @@ +export { pack, verifyPackageDir, type PackOptions, type PackResult } from "./pack.js"; +export { + detectExecutableKind, + isNativeKind, + parseShebangInterpreter, + type ExecutableKind, +} from "./header.js"; diff --git a/packages/agentos-toolchain/src/pack.ts b/packages/agentos-toolchain/src/pack.ts new file mode 100644 index 000000000..da1785801 --- /dev/null +++ b/packages/agentos-toolchain/src/pack.ts @@ -0,0 +1,299 @@ +import { execFileSync } from "node:child_process"; +import { + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + openSync, + readFileSync, + readSync, + closeSync, + readdirSync, + rmSync, + statSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join, relative, resolve } from "node:path"; +import { detectExecutableKind, isNativeKind } from "./header.js"; + +export interface PackOptions { + /** npm package spec (`name`, `name@version`) or a local directory path. */ + source: string; + /** + * Output dir for the package itself (FLAT): the package lands directly at + * `/{bin/, node_modules/, package.json}`. The versioned + * `/opt/agentos//` + `current` layout is the sidecar + * projection's job (name from the descriptor, version from this package.json). + */ + out: string; + /** Mark a bin command as the package's ACP entrypoint (validated against bin/). */ + agent?: string; + /** + * Delete native `.node` addons from the flat closure instead of failing. + * Use when a package's dependency tree contains optional/platform native + * addons (e.g. `koffi`, `clipboard-*`) that are never loaded on the code path + * that runs in V8. The JS closure (including dynamically-required modules) is + * kept; only the V8-incompatible `.node` files are removed. If a pruned addon + * IS reached at runtime it fails then — so this is opt-in. + */ + pruneNative?: boolean; +} + +export interface PackResult { + name: string; + version: string; + packageDir: string; + commands: string[]; +} + +const HEAD_BYTES = 256; // BINPRM_BUF_SIZE-sized header read + +function readHead(file: string): Buffer { + const fd = openSync(file, "r"); + try { + const buf = Buffer.alloc(HEAD_BYTES); + const n = readSync(fd, buf, 0, HEAD_BYTES, 0); + return buf.subarray(0, n); + } finally { + closeSync(fd); + } +} + +function npmInstallFlat(source: string, into: string): void { + mkdirSync(join(into, "node_modules"), { recursive: true }); + // `source` here is already a copy-forcing install spec (a tarball for local + // dirs, via `resolveInstallSpec`) — never a bare directory, which npm would + // symlink as a depless `file:` link. A flat, production-only install: full + // closure, no symlinked store, no scripts. + execFileSync( + "npm", + [ + "install", + source, + "--omit=dev", + "--ignore-scripts", + "--no-audit", + "--no-fund", + "--no-package-lock", + "--install-strategy=hoisted", + "--prefix", + into, + ], + { stdio: "pipe" }, + ); +} + +/** + * Resolve a pack source into an install spec npm will COPY (not symlink). + * + * `npm install ` adds the directory as a `file:` link — it symlinks + * the package into `node_modules` and does NOT install its dependency closure, + * which yields an empty, source-symlinked "package" instead of a self-contained + * one. Packing the directory into a tarball first (honoring its `files` field) + * and installing THAT makes npm extract a real copy and resolve the full + * dependency tree. npm specs (`name`, `name@version`) are returned unchanged. + */ +function resolveInstallSpec(source: string, scratch: string): string { + if (!(existsSync(source) && statSync(source).isDirectory())) return source; + const dest = join(scratch, "tarball"); + mkdirSync(dest, { recursive: true }); + const out = execFileSync( + "npm", + ["pack", resolve(source), "--pack-destination", dest, "--ignore-scripts", "--silent"], + { encoding: "utf8" }, + ); + const file = out.trim().split("\n").pop()?.trim(); + if (!file) throw new Error(`npm pack produced no tarball for ${source}`); + return join(dest, file); +} + +/** Resolve the installed package's name from a source spec or local dir. */ +function installedPackageName(source: string): string { + if (existsSync(source) && statSync(source).isDirectory()) { + const pkg = JSON.parse(readFileSync(join(source, "package.json"), "utf8")); + return pkg.name as string; + } + // `name`, `name@version`, `@scope/name@version` + const at = source.lastIndexOf("@"); + return at > 0 ? source.slice(0, at) : source; +} + +/** Normalize a package.json `bin` field to `{ commandName: relativeEntryPath }`. */ +function binEntries(pkgDir: string): Record { + const pkg = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf8")); + const bin = pkg.bin; + if (!bin) return {}; + if (typeof bin === "string") { + return { [pkg.name.replace(/^@[^/]+\//, "")]: bin }; + } + return bin as Record; +} + +export function findNativeAddons(root: string): string[] { + const hits: string[] = []; + const walk = (dir: string) => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, entry.name); + if (entry.isDirectory()) walk(p); + else if (entry.isFile() && entry.name.endsWith(".node")) hits.push(p); + } + }; + if (existsSync(root)) walk(root); + return hits; +} + +/** + * Verify a finished package directory satisfies the agentOS package format: + * every `bin/` entry has a recognized, non-native header, and the tree has no + * native `.node` addons. Throws with a clear message on the first violation. + */ +export function verifyPackageDir(packageDir: string): void { + const pkgJsonPath = join(packageDir, "package.json"); + if (!existsSync(pkgJsonPath)) { + throw new Error(`missing required package.json in ${packageDir}`); + } + let version: unknown; + try { + version = JSON.parse(readFileSync(pkgJsonPath, "utf8")).version; + } catch (error) { + throw new Error( + `package.json in ${packageDir} is not valid JSON: ${String(error)}`, + ); + } + if (typeof version !== "string" || version.length === 0) { + throw new Error(`package.json in ${packageDir} is missing a valid "version"`); + } + const binDir = join(packageDir, "bin"); + if (!existsSync(binDir)) { + throw new Error(`package has no bin/ directory: ${packageDir}`); + } + for (const entry of readdirSync(binDir)) { + const target = resolve(binDir, entry); // follows the symlink + const kind = detectExecutableKind(readHead(target)); + if (isNativeKind(kind)) { + throw new Error( + `bin/${entry} is a native ${kind} binary, which cannot run in agentOS`, + ); + } + if (kind === "unknown") { + throw new Error( + `bin/${entry} has no recognized header — JS/script commands need a '#!' shebang`, + ); + } + } + const addons = findNativeAddons(join(packageDir, "node_modules")); + if (addons.length > 0) { + throw new Error( + `package contains native .node addon(s) that won't load in V8: ${addons + .map((a) => relative(packageDir, a)) + .join(", ")}; re-run with --prune-native to drop them if they are unreachable on the V8 code path`, + ); + } +} + +/** + * Resolve a package's `bin` entry to its real on-disk location in the packed + * closure. A bin path like `./node_modules//` is written RELATIVE to the + * declaring package's root, but a flat (`npm install`) install HOISTS shared deps + * to the top-level `node_modules`, so the literal nested path may not exist. A + * wrapper package (e.g. `pi-cli`) whose `bin` points into its dependencies is the + * common case. Try the nested path first, then the hoisted top-level copy. + */ +function resolveBinTarget( + closureModules: string, + name: string, + entryRel: string, +): string { + const nested = join(closureModules, name, entryRel); + if (existsSync(nested)) return nested; + // `./node_modules//` → hoisted `/node_modules//`. + const hoistedMatch = entryRel.match(/^\.?\/?node_modules\/(.+)$/); + if (hoistedMatch) { + const hoisted = join(closureModules, hoistedMatch[1]); + if (existsSync(hoisted)) return hoisted; + } + throw new Error( + `pack: cannot resolve bin target "${entryRel}" for ${name} — not found nested ` + + `(${nested}) or hoisted. The declaring package's bin path does not match the ` + + `installed (hoisted) layout.`, + ); +} + +export function pack(options: PackOptions): PackResult { + const { source, out, agent, pruneNative } = options; + const tmp = mkdtempSync(join(tmpdir(), "agentos-pack-")); + try { + npmInstallFlat(resolveInstallSpec(source, tmp), tmp); + + const name = installedPackageName(source); + const installedDir = join(tmp, "node_modules", name); + const pkg = JSON.parse(readFileSync(join(installedDir, "package.json"), "utf8")); + const version: string = pkg.version; + const bins = binEntries(installedDir); + const commands = Object.keys(bins); + if (commands.length === 0) { + throw new Error(`package "${name}" declares no bin commands`); + } + if (agent && !commands.includes(agent)) { + throw new Error( + `--agent "${agent}" is not one of the package's commands: ${commands.join(", ")}`, + ); + } + + // Flat output: the package IS `out`. The versioned `/opt/agentos// + // ` + `current` layout is the sidecar projection's job. + const packageDir = out; + rmSync(packageDir, { recursive: true, force: true }); + mkdirSync(packageDir, { recursive: true }); + + const binDir = join(packageDir, "bin"); + mkdirSync(binDir, { recursive: true }); + + // Flat closure (includes the package itself under node_modules/). + cpSync(join(tmp, "node_modules"), join(packageDir, "node_modules"), { + recursive: true, + }); + // bin/ → ../node_modules// (relative symlink; node resolves + // deps from the realpath's node_modules). + const closureModules = join(packageDir, "node_modules"); + for (const [cmd, entryRel] of Object.entries(bins)) { + const targetAbs = resolveBinTarget(closureModules, name, entryRel); + symlinkSync(relative(binDir, targetAbs), join(binDir, cmd)); + } + if (pruneNative) { + // Remove V8-incompatible native addons from the kept JS closure. The + // rest of node_modules (including dynamically-required modules) stays. + const addons = findNativeAddons(join(packageDir, "node_modules")); + for (const addon of addons) { + rmSync(addon, { force: true }); + } + if (addons.length > 0) { + console.warn( + `[agentos-toolchain] pruned ${addons.length} native .node addon(s) from ${name} (--prune-native); they must be unreachable on the V8 code path`, + ); + } + } + + // Write a normal root package.json {name, version, bin}. The sidecar reads + // `version` from here (there is no agentos-package.json manifest anymore); + // `bin` maps each command to its projected `bin/` so the package is a + // valid, self-describing npm package. `--agent` is validated against the + // commands above; the `agent` block now lives in the host `defineSoftware` + // descriptor, not in the package. + const binMap: Record = {}; + for (const cmd of commands) { + binMap[cmd] = `bin/${cmd}`; + } + writeFileSync( + join(packageDir, "package.json"), + `${JSON.stringify({ name, version, bin: binMap }, null, 2)}\n`, + ); + + verifyPackageDir(packageDir); + return { name, version, packageDir, commands }; + } finally { + rmSync(tmp, { recursive: true, force: true }); + } +} diff --git a/packages/agentos-toolchain/tests/pack.test.ts b/packages/agentos-toolchain/tests/pack.test.ts new file mode 100644 index 000000000..44727712b --- /dev/null +++ b/packages/agentos-toolchain/tests/pack.test.ts @@ -0,0 +1,173 @@ +import { execFileSync } from "node:child_process"; +import { + chmodSync, + lstatSync, + mkdirSync, + mkdtempSync, + readFileSync, + readlinkSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, test } from "vitest"; +import { detectExecutableKind, isNativeKind, parseShebangInterpreter } from "../src/header.js"; +import { findNativeAddons, pack, verifyPackageDir } from "../src/pack.js"; + +function hasWorkingNpm(): boolean { + try { + execFileSync("npm", ["--version"], { stdio: "ignore" }); + return true; + } catch { + return false; + } +} +const npmOk = hasWorkingNpm(); + +const dirs: string[] = []; +const mkTmp = (p: string) => { + const d = mkdtempSync(join(tmpdir(), p)); + dirs.push(d); + return d; +}; +afterEach(() => { + for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }); +}); + +describe("header detection", () => { + test("recognizes shebang, wasm, and native magics", () => { + expect(detectExecutableKind(Buffer.from("#!/usr/bin/env node\n"))).toBe("shebang"); + expect(detectExecutableKind(Buffer.from([0x00, 0x61, 0x73, 0x6d]))).toBe("wasm"); + expect(detectExecutableKind(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))).toBe("native-elf"); + expect(detectExecutableKind(Buffer.from([0xcf, 0xfa, 0xed, 0xfe]))).toBe("native-macho"); + expect(detectExecutableKind(Buffer.from([0x4d, 0x5a, 0x90, 0x00]))).toBe("native-pe"); + expect(detectExecutableKind(Buffer.from("just text"))).toBe("unknown"); + }); + + test("cafebabe Java class is not mis-detected as Mach-O", () => { + const java = Buffer.from([0xca, 0xfe, 0xba, 0xbe, 0x00, 0x00, 0x00, 0x34]); // Java 8 + expect(detectExecutableKind(java)).toBe("unknown"); + const fat = Buffer.from([0xca, 0xfe, 0xba, 0xbe, 0x00, 0x00, 0x00, 0x02]); // Mach-O fat + expect(detectExecutableKind(fat)).toBe("native-macho"); + }); + + test("isNativeKind + shebang interpreter parsing", () => { + expect(isNativeKind("native-elf")).toBe(true); + expect(isNativeKind("shebang")).toBe(false); + expect(parseShebangInterpreter(Buffer.from("#!/usr/bin/env python3\n"))).toBe("/usr/bin/env"); + expect(parseShebangInterpreter(Buffer.from("not a shebang"))).toBeNull(); + }); +}); + +/** Build a FLAT package dir by hand (no npm) to exercise verifyPackageDir. */ +function handBuiltPackage(name = "pkg", version = "1.0.0"): string { + const pkgDir = mkTmp("agentos-pkg-"); + mkdirSync(join(pkgDir, "bin"), { recursive: true }); + mkdirSync(join(pkgDir, "node_modules"), { recursive: true }); + writeFileSync( + join(pkgDir, "package.json"), + `${JSON.stringify({ name, version, bin: { tool: "bin/tool" } }, null, 2)}\n`, + ); + writeFileSync(join(pkgDir, "bin", "tool"), "#!/usr/bin/env node\nconsole.log('ok');\n"); + chmodSync(join(pkgDir, "bin", "tool"), 0o755); + return pkgDir; +} + +describe("verifyPackageDir", () => { + test("accepts a well-formed package", () => { + expect(() => verifyPackageDir(handBuiltPackage())).not.toThrow(); + }); + + test("rejects a missing package.json", () => { + const pkgDir = handBuiltPackage(); + rmSync(join(pkgDir, "package.json")); + expect(() => verifyPackageDir(pkgDir)).toThrow(/package\.json/); + }); + + test("rejects native .node addons", () => { + const pkgDir = handBuiltPackage("withaddon"); + writeFileSync(join(pkgDir, "node_modules", "evil.node"), "binary"); + expect(() => verifyPackageDir(pkgDir)).toThrow(/native \.node addon/); + // The error must name the --prune-native escape hatch. + expect(() => verifyPackageDir(pkgDir)).toThrow(/--prune-native/); + }); + + test("findNativeAddons locates .node files; pruning them lets verify pass", () => { + const pkgDir = handBuiltPackage("prunable"); + const nm = join(pkgDir, "node_modules"); + mkdirSync(join(nm, "koffi", "build"), { recursive: true }); + writeFileSync(join(nm, "koffi", "build", "koffi.node"), "native"); + writeFileSync(join(nm, "clipboard.node"), "native"); + const addons = findNativeAddons(nm); + expect(addons.length).toBe(2); + expect(() => verifyPackageDir(pkgDir)).toThrow(/native \.node addon/); + // Prune them (what --prune-native does) → verify now passes. + for (const addon of addons) rmSync(addon); + expect(() => verifyPackageDir(pkgDir)).not.toThrow(); + }); + + test("rejects a headerless bin command", () => { + const pkgDir = handBuiltPackage("bad"); + writeFileSync(join(pkgDir, "bin", "nohdr"), "plain text, no shebang"); + expect(() => verifyPackageDir(pkgDir)).toThrow(/no recognized header/); + }); + + test("rejects a native bin command", () => { + const pkgDir = handBuiltPackage("native"); + writeFileSync(join(pkgDir, "bin", "elf"), Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0, 0])); + expect(() => verifyPackageDir(pkgDir)).toThrow(/native native-elf/); + }); +}); + +function makeFixture(name = "hello", version = "1.2.3"): string { + const src = mkTmp("agentos-fixture-"); + writeFileSync( + join(src, "package.json"), + JSON.stringify({ name, version, bin: { hello: "bin/hello.js" } }), + ); + mkdirSync(join(src, "bin"), { recursive: true }); + writeFileSync(join(src, "bin", "hello.js"), "#!/usr/bin/env node\nconsole.log('hi');\n"); + chmodSync(join(src, "bin", "hello.js"), 0o755); + return src; +} + +// These exercise the real `npm install` flow; gated on a working npm (present in +// any npx/CI environment, absent in this minimal pnpm-only sandbox). +describe.skipIf(!npmOk)("pack (offline, local fixture, needs npm)", () => { + test("packs a zero-dep local package into a flat valid agentOS package", () => { + const src = makeFixture(); + const out = mkTmp("agentos-out-"); + const result = pack({ source: src, out }); + + expect(result.name).toBe("hello"); + expect(result.version).toBe("1.2.3"); + expect(result.commands).toEqual(["hello"]); + expect(result.packageDir).toBe(out); + + // Flat output: the package lands directly in `out` (no //). + expect(JSON.parse(readFileSync(join(out, "package.json"), "utf8"))).toEqual({ + name: "hello", + version: "1.2.3", + bin: { hello: "bin/hello" }, + }); + const binLink = join(out, "bin", "hello"); + expect(lstatSync(binLink).isSymbolicLink()).toBe(true); + expect(readlinkSync(binLink)).toContain("node_modules/hello/bin/hello.js"); + expect(() => verifyPackageDir(out)).not.toThrow(); + }); + + test("--agent validates the entrypoint against the package commands", () => { + const src = makeFixture("agentpkg", "0.1.0"); + const out = mkTmp("agentos-out-"); + pack({ source: src, out, agent: "hello" }); + // No agentos-package.json; the entrypoint is a real bin command + in package.json bin. + const pkg = JSON.parse(readFileSync(join(out, "package.json"), "utf8")); + expect(pkg.bin).toHaveProperty("hello"); + expect(lstatSync(join(out, "bin", "hello")).isSymbolicLink()).toBe(true); + // An entrypoint that is not a command is rejected. + expect(() => pack({ source: src, out: mkTmp("agentos-out-"), agent: "nope" })).toThrow( + /--agent "nope" is not one of/, + ); + }); +}); diff --git a/packages/agentos-toolchain/tsconfig.json b/packages/agentos-toolchain/tsconfig.json new file mode 100644 index 000000000..a5d3ddbee --- /dev/null +++ b/packages/agentos-toolchain/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "types": ["node"], + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "tests"] +} diff --git a/packages/build-tools/package.json b/packages/build-tools/package.json index fa322885b..c43417bca 100644 --- a/packages/build-tools/package.json +++ b/packages/build-tools/package.json @@ -12,8 +12,7 @@ "build:base-filesystem": "node ./scripts/build-base-filesystem.mjs", "build:protocol": "node ./scripts/compile-sidecar-protocol.mjs", "build:v8-bridge": "node ./scripts/build-v8-bridge.mjs", - "snapshot:alpine-defaults": "node ./scripts/snapshot-alpine-defaults.mjs", - "check-types": "node --check ./scripts/build-base-filesystem.mjs && node --check ./scripts/build-v8-bridge.mjs && node --check ./scripts/compile-sidecar-protocol.mjs && node --check ./scripts/snapshot-alpine-defaults.mjs", + "check-types": "node --check ./scripts/build-base-filesystem.mjs && node --check ./scripts/build-v8-bridge.mjs && node --check ./scripts/compile-sidecar-protocol.mjs", "build": "pnpm run check-types", "test": "pnpm run check-types" }, diff --git a/packages/build-tools/scripts/build-base-filesystem.mjs b/packages/build-tools/scripts/build-base-filesystem.mjs index 865564ead..af746199e 100644 --- a/packages/build-tools/scripts/build-base-filesystem.mjs +++ b/packages/build-tools/scripts/build-base-filesystem.mjs @@ -1,15 +1,29 @@ #!/usr/bin/env node -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +// Build the secure-exec base filesystem in ONE step: snapshot a stock Alpine +// container, apply the secure-exec transforms, and write the single +// `base-filesystem.json`. Requires Docker. Run this BY HAND when the base needs +// updating — nothing runs it during a build. +// +// There is exactly ONE committed copy: crates/vfs/assets/base-filesystem.json. +// The vfs crate embeds it directly via `include_str!`; the sidecar reads it via +// `vfs::posix::base_filesystem_json()`; the host bakes the env in as a constant +// (packages/core/src/base-filesystem.ts) and reads no JSON. If you change the env +// here, update that constant to match. + +import { execFileSync } from "node:child_process"; +import { mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -const DEFAULT_INPUT = fileURLToPath( - new URL("../../core/fixtures/alpine-defaults.json", import.meta.url), -); -const DEFAULT_OUTPUT = fileURLToPath( - new URL("../../core/fixtures/base-filesystem.json", import.meta.url), -); +const DEFAULT_IMAGE = process.env.ALPINE_IMAGE ?? "alpine:3.22"; + +// The ONE committed copy — embedded into the vfs crate via include_str!. +const OUTPUT_PATHS = [ + fileURLToPath(new URL("../../../crates/vfs/assets/base-filesystem.json", import.meta.url)), +]; + +// --- secure-exec base identity (the transform target) ----------------------- const BASE_HOSTNAME = "secure-exec"; const BASE_USER = "agentos"; @@ -25,64 +39,254 @@ const EXTRA_DIRECTORIES = [ { path: "/workspace", type: "directory", mode: "755", uid: BASE_UID, gid: BASE_GID }, ]; -function readJson(pathname) { - return JSON.parse(readFileSync(pathname, "utf-8")); +const TRANSFORMS = [ + "Normalize HOSTNAME to secure-exec", + "Preserve the captured user-level environment and filesystem layout as the secure-exec base layer", + "Add the non-Alpine /workspace directory (default agent working directory) owned by the base user", +]; + +// --- Alpine snapshot capture (Docker) --------------------------------------- + +const DEFAULT_ENV_KEYS = [ + "CHARSET", + "HOME", + "HOSTNAME", + "LANG", + "LC_COLLATE", + "LOGNAME", + "PAGER", + "PATH", + "SHELL", + "USER", +]; + +const TEXT_FILE_PATHS = [ + "/etc/alpine-release", + "/etc/group", + "/etc/hostname", + "/etc/nsswitch.conf", + "/etc/passwd", + "/etc/profile", + "/etc/shadow", + "/etc/shells", + "/usr/lib/os-release", +]; + +const METADATA_ONLY_FILE_PATHS = ["/usr/bin/env"]; + +const SYMLINK_PATHS = [ + "/etc/os-release", + "/var/lock", + "/var/run", + "/var/spool/cron/crontabs", +]; + +function addParentDirectories(paths) { + for (const entry of [...paths]) { + let current = path.posix.dirname(entry); + while (current !== "." && current !== "/") { + paths.add(current); + current = path.posix.dirname(current); + } + } } -function normalizeEntry(entry) { - if (entry.path === "/etc/hostname" && entry.type === "file") { +function runDocker(args, options = {}) { + return execFileSync("docker", args, { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + ...options, + }); +} + +function dockerExec(containerId, args) { + return runDocker(["exec", containerId, ...args]); +} + +function parseEnv(stdout) { + const result = {}; + for (const line of stdout.split("\n")) { + if (!line.trim()) { + continue; + } + const separator = line.indexOf("="); + if (separator === -1) { + continue; + } + result[line.slice(0, separator)] = line.slice(separator + 1); + } + return result; +} + +function mapFileType(type) { + switch (type) { + case "directory": + return "directory"; + case "regular file": + return "file"; + case "symbolic link": + return "symlink"; + default: + throw new Error(`Unsupported file type: ${type}`); + } +} + +function shouldIncludePath(path) { + if (path === "/dev" || path.startsWith("/dev/")) { + return false; + } + if (path === "/proc" || path.startsWith("/proc/")) { + return false; + } + if (path.startsWith("/sys/")) { + return false; + } + if (path === "/etc/mtab") { + return false; + } + return true; +} + +function collectPaths(containerId) { + const discovered = dockerExec(containerId, [ + "sh", + "-lc", + "find / -maxdepth 2 -type d | sort", + ]); + + const paths = new Set( + discovered + .split("\n") + .map((path) => path.trim()) + .filter(Boolean) + .filter(shouldIncludePath), + ); + + for (const path of TEXT_FILE_PATHS) { + paths.add(path); + } + for (const path of METADATA_ONLY_FILE_PATHS) { + paths.add(path); + } + for (const path of SYMLINK_PATHS) { + paths.add(path); + } + paths.add("/"); + addParentDirectories(paths); + + return [...paths].sort((a, b) => a.localeCompare(b)); +} + +function readEntry(containerId, path) { + const statOutput = dockerExec(containerId, [ + "sh", + "-lc", + `stat -c '%F\t%a\t%u\t%g' '${path}'`, + ]).trim(); + const [rawType, mode, uid, gid] = statOutput.split("\t"); + const entry = { + path, + type: mapFileType(rawType), + mode, + uid: Number(uid), + gid: Number(gid), + }; + + if (entry.type === "symlink") { + entry.target = dockerExec(containerId, ["readlink", path]).trim(); + } + + if (entry.type === "file" && TEXT_FILE_PATHS.includes(path)) { + entry.content = dockerExec(containerId, ["cat", path]); + } + + return entry; +} + +function extractPrompt(profileContent) { + const match = profileContent.match(/PS1='([^']+)'/); + if (!match) { + throw new Error("Unable to extract PS1 from /etc/profile"); + } + return match[1]; +} + +function captureAlpineSnapshot() { + const containerId = runDocker([ + "run", + "--detach", + "--rm", + DEFAULT_IMAGE, + "sh", + "-lc", + "adduser -D agentos >/dev/null 2>&1 && sleep infinity", + ]).trim(); + + try { + const env = parseEnv(dockerExec(containerId, ["sh", "-lc", "su agentos -c env"])); + const profileContent = dockerExec(containerId, ["cat", "/etc/profile"]); + const entries = collectPaths(containerId).map((path) => readEntry(containerId, path)); return { - ...entry, - content: `${BASE_HOSTNAME}\n`, + env: Object.fromEntries( + DEFAULT_ENV_KEYS.filter((key) => env[key] !== undefined).map((key) => [key, env[key]]), + ), + prompt: extractPrompt(profileContent), + entries, }; + } finally { + try { + runDocker(["rm", "--force", containerId]); + } catch { + // Container may already be gone. + } } +} + +// --- transform: raw Alpine snapshot -> secure-exec base filesystem ---------- +function normalizeEntry(entry) { + if (entry.path === "/etc/hostname" && entry.type === "file") { + return { ...entry, content: `${BASE_HOSTNAME}\n` }; + } return entry; } function withExtraDirectories(entries) { const existing = new Set(entries.map((entry) => entry.path)); - const additions = EXTRA_DIRECTORIES.filter((entry) => !existing.has(entry.path)); - return [...entries, ...additions]; + return [...entries, ...EXTRA_DIRECTORIES.filter((entry) => !existing.has(entry.path))]; } -function buildBaseFilesystem(snapshot, inputPath) { +function buildBaseFilesystem(snapshot) { return { source: { - snapshotPath: path.basename(inputPath), - image: snapshot.image, - snapshotCreatedAt: snapshot.createdAt, + image: DEFAULT_IMAGE, builtAt: new Date().toISOString(), - transforms: [ - "Normalize HOSTNAME to secure-exec", - "Preserve the captured user-level environment and filesystem layout as the secure-exec base layer", - "Add the non-Alpine /workspace directory (default agent working directory) owned by the base user", - ], + transforms: TRANSFORMS, }, environment: { env: { - ...snapshot.environment.env, + ...snapshot.env, HOME: BASE_HOME, HOSTNAME: BASE_HOSTNAME, LOGNAME: BASE_USER, USER: BASE_USER, }, - prompt: snapshot.environment.prompt, + prompt: snapshot.prompt, }, filesystem: { - entries: withExtraDirectories(snapshot.filesystem.entries.map(normalizeEntry)), + entries: withExtraDirectories(snapshot.entries.map(normalizeEntry)), }, }; } function main() { - const [inputPath = DEFAULT_INPUT, outputPath = DEFAULT_OUTPUT] = process.argv.slice(2); - const snapshot = readJson(inputPath); - const baseFilesystem = buildBaseFilesystem(snapshot, inputPath); - - mkdirSync(path.dirname(outputPath), { recursive: true }); - writeFileSync(outputPath, `${JSON.stringify(baseFilesystem, null, 2)}\n`); - process.stdout.write(`Wrote ${outputPath}\n`); + const baseFilesystem = buildBaseFilesystem(captureAlpineSnapshot()); + const json = `${JSON.stringify(baseFilesystem, null, 2)}\n`; + for (const outputPath of OUTPUT_PATHS) { + mkdirSync(path.dirname(outputPath), { recursive: true }); + writeFileSync(outputPath, json); + process.stdout.write(`Wrote ${outputPath}\n`); + } } main(); diff --git a/packages/build-tools/scripts/snapshot-alpine-defaults.mjs b/packages/build-tools/scripts/snapshot-alpine-defaults.mjs deleted file mode 100644 index 5850d7e99..000000000 --- a/packages/build-tools/scripts/snapshot-alpine-defaults.mjs +++ /dev/null @@ -1,236 +0,0 @@ -#!/usr/bin/env node - -import { execFileSync } from "node:child_process"; -import { mkdirSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const DEFAULT_IMAGE = process.env.ALPINE_IMAGE ?? "alpine:3.22"; -const DEFAULT_OUTPUT = fileURLToPath( - new URL("../../core/fixtures/alpine-defaults.json", import.meta.url), -); - -const DEFAULT_ENV_KEYS = [ - "CHARSET", - "HOME", - "HOSTNAME", - "LANG", - "LC_COLLATE", - "LOGNAME", - "PAGER", - "PATH", - "SHELL", - "USER", -]; - -const TEXT_FILE_PATHS = [ - "/etc/alpine-release", - "/etc/group", - "/etc/hostname", - "/etc/nsswitch.conf", - "/etc/passwd", - "/etc/profile", - "/etc/shadow", - "/etc/shells", - "/usr/lib/os-release", -]; - -const METADATA_ONLY_FILE_PATHS = [ - "/usr/bin/env", -]; - -const SYMLINK_PATHS = [ - "/etc/os-release", - "/var/lock", - "/var/run", - "/var/spool/cron/crontabs", -]; - -function addParentDirectories(paths) { - for (const entry of [...paths]) { - let current = path.posix.dirname(entry); - while (current !== "." && current !== "/") { - paths.add(current); - current = path.posix.dirname(current); - } - } -} - -function runDocker(args, options = {}) { - return execFileSync("docker", args, { - encoding: "utf-8", - stdio: ["ignore", "pipe", "pipe"], - ...options, - }); -} - -function dockerExec(containerId, args) { - return runDocker(["exec", containerId, ...args]); -} - -function parseEnv(stdout) { - const result = {}; - - for (const line of stdout.split("\n")) { - if (!line.trim()) { - continue; - } - - const separator = line.indexOf("="); - if (separator === -1) { - continue; - } - - const key = line.slice(0, separator); - const value = line.slice(separator + 1); - result[key] = value; - } - - return result; -} - -function mapFileType(type) { - switch (type) { - case "directory": - return "directory"; - case "regular file": - return "file"; - case "symbolic link": - return "symlink"; - default: - throw new Error(`Unsupported file type: ${type}`); - } -} - -function shouldIncludePath(path) { - if (path === "/dev" || path.startsWith("/dev/")) { - return false; - } - if (path === "/proc" || path.startsWith("/proc/")) { - return false; - } - if (path.startsWith("/sys/")) { - return false; - } - if (path === "/etc/mtab") { - return false; - } - return true; -} - -function collectPaths(containerId) { - const discovered = dockerExec(containerId, [ - "sh", - "-lc", - "find / -maxdepth 2 -type d | sort", - ]); - - const paths = new Set( - discovered - .split("\n") - .map((path) => path.trim()) - .filter(Boolean) - .filter(shouldIncludePath), - ); - - for (const path of TEXT_FILE_PATHS) { - paths.add(path); - } - for (const path of METADATA_ONLY_FILE_PATHS) { - paths.add(path); - } - for (const path of SYMLINK_PATHS) { - paths.add(path); - } - paths.add("/"); - addParentDirectories(paths); - - return [...paths].sort((a, b) => a.localeCompare(b)); -} - -function readEntry(containerId, path) { - const statOutput = dockerExec(containerId, [ - "sh", - "-lc", - `stat -c '%F\t%a\t%u\t%g' '${path}'`, - ]).trim(); - const [rawType, mode, uid, gid] = statOutput.split("\t"); - const entry = { - path, - type: mapFileType(rawType), - mode, - uid: Number(uid), - gid: Number(gid), - }; - - if (entry.type === "symlink") { - entry.target = dockerExec(containerId, ["readlink", path]).trim(); - } - - if (entry.type === "file" && TEXT_FILE_PATHS.includes(path)) { - entry.content = dockerExec(containerId, ["cat", path]); - } - - return entry; -} - -function extractPrompt(profileContent) { - const match = profileContent.match(/PS1='([^']+)'/); - if (!match) { - throw new Error("Unable to extract PS1 from /etc/profile"); - } - return match[1]; -} - -function main() { - const outputPath = process.argv[2] ?? DEFAULT_OUTPUT; - const containerId = runDocker([ - "run", - "--detach", - "--rm", - DEFAULT_IMAGE, - "sh", - "-lc", - "adduser -D agentos >/dev/null 2>&1 && sleep infinity", - ]).trim(); - - try { - const rawEnv = parseEnv( - dockerExec(containerId, ["sh", "-lc", "su agentos -c env"]), - ); - const env = Object.fromEntries( - DEFAULT_ENV_KEYS - .filter((key) => rawEnv[key] !== undefined) - .map((key) => [key, rawEnv[key]]), - ); - - const profileContent = dockerExec(containerId, ["cat", "/etc/profile"]); - const entries = collectPaths(containerId).map((path) => - readEntry(containerId, path), - ); - - const snapshot = { - image: DEFAULT_IMAGE, - createdAt: new Date().toISOString(), - environment: { - env, - prompt: extractPrompt(profileContent), - }, - filesystem: { - entries, - }, - }; - - mkdirSync(path.dirname(outputPath), { recursive: true }); - writeFileSync(outputPath, `${JSON.stringify(snapshot, null, 2)}\n`); - process.stdout.write(`Wrote ${outputPath}\n`); - } finally { - try { - runDocker(["rm", "--force", containerId]); - } catch { - // Container may already be gone. - } - } -} - -main(); diff --git a/packages/core/fixtures/alpine-defaults.json b/packages/core/fixtures/alpine-defaults.json deleted file mode 100644 index 2a417ab38..000000000 --- a/packages/core/fixtures/alpine-defaults.json +++ /dev/null @@ -1,520 +0,0 @@ -{ - "image": "alpine:3.22", - "createdAt": "2026-06-22T19:45:20.529Z", - "environment": { - "env": { - "CHARSET": "UTF-8", - "HOME": "/home/agentos", - "HOSTNAME": "13d6f60ded74", - "LANG": "C.UTF-8", - "LC_COLLATE": "C", - "LOGNAME": "agentos", - "PAGER": "less", - "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", - "SHELL": "/bin/sh", - "USER": "agentos" - }, - "prompt": "\\h:\\w\\$ " - }, - "filesystem": { - "entries": [ - { - "path": "/", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/bin", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/alpine-release", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "3.22.4\n" - }, - { - "path": "/etc/apk", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/busybox-paths.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/crontabs", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/group", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "root:x:0:root\nbin:x:1:root,bin,daemon\ndaemon:x:2:root,bin,daemon\nsys:x:3:root,bin\nadm:x:4:root,daemon\ntty:x:5:\ndisk:x:6:root\nlp:x:7:lp\nkmem:x:9:\nwheel:x:10:root\nfloppy:x:11:root\nmail:x:12:mail\nnews:x:13:news\nuucp:x:14:uucp\ncron:x:16:cron\naudio:x:18:\ncdrom:x:19:\ndialout:x:20:root\nftp:x:21:\nsshd:x:22:\ninput:x:23:\ntape:x:26:root\nvideo:x:27:root\nnetdev:x:28:\nkvm:x:34:kvm\ngames:x:35:\nshadow:x:42:\nwww-data:x:82:\nusers:x:100:games\nntp:x:123:\nabuild:x:300:\nutmp:x:406:\nping:x:999:\nnogroup:x:65533:\nnobody:x:65534:\nagentos:x:1000:\n" - }, - { - "path": "/etc/hostname", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "13d6f60ded74\n" - }, - { - "path": "/etc/logrotate.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/modprobe.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/modules-load.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/network", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/nsswitch.conf", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "# musl itself does not support NSS, however some third-party DNS\n# implementations use the nsswitch.conf file to determine what\n# policy to follow.\n# Editing this file is not recommended.\nhosts: files dns\n" - }, - { - "path": "/etc/opt", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/os-release", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "../usr/lib/os-release" - }, - { - "path": "/etc/passwd", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "root:x:0:0:root:/root:/bin/sh\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaemon:x:2:2:daemon:/sbin:/sbin/nologin\nlp:x:4:7:lp:/var/spool/lpd:/sbin/nologin\nsync:x:5:0:sync:/sbin:/bin/sync\nshutdown:x:6:0:shutdown:/sbin:/sbin/shutdown\nhalt:x:7:0:halt:/sbin:/sbin/halt\nmail:x:8:12:mail:/var/mail:/sbin/nologin\nnews:x:9:13:news:/usr/lib/news:/sbin/nologin\nuucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin\ncron:x:16:16:cron:/var/spool/cron:/sbin/nologin\nftp:x:21:21::/var/lib/ftp:/sbin/nologin\nsshd:x:22:22:sshd:/dev/null:/sbin/nologin\ngames:x:35:35:games:/usr/games:/sbin/nologin\nntp:x:123:123:NTP:/var/empty:/sbin/nologin\nguest:x:405:100:guest:/dev/null:/sbin/nologin\nnobody:x:65534:65534:nobody:/:/sbin/nologin\nagentos:x:1000:1000::/home/agentos:/bin/sh\n" - }, - { - "path": "/etc/periodic", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/profile", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "export PATH=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n\nexport PAGER=less\numask 022\n\n# use nicer PS1 for bash and busybox ash\nif [ -n \"$BASH_VERSION\" -o \"$BB_ASH_VERSION\" ]; then\n\tPS1='\\h:\\w\\$ '\n# use nicer PS1 for zsh\nelif [ -n \"$ZSH_VERSION\" ]; then\n\tPS1='%m:%~%# '\n# set up fallback default PS1\nelse\n\t: \"${HOSTNAME:=$(hostname)}\"\n\tPS1='${HOSTNAME%%.*}:$PWD'\n\t[ \"$(id -u)\" -eq 0 ] && PS1=\"${PS1}# \" || PS1=\"${PS1}\\$ \"\nfi\n\nfor script in /etc/profile.d/*.sh ; do\n\tif [ -r \"$script\" ] ; then\n\t\t. \"$script\"\n\tfi\ndone\nunset script\n" - }, - { - "path": "/etc/profile.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/secfixes.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/shadow", - "type": "file", - "mode": "640", - "uid": 0, - "gid": 42, - "content": "root:*::0:::::\nbin:!::0:::::\ndaemon:!::0:::::\nlp:!::0:::::\nsync:!::0:::::\nshutdown:!::0:::::\nhalt:!::0:::::\nmail:!::0:::::\nnews:!::0:::::\nuucp:!::0:::::\ncron:!::0:::::\nftp:!::0:::::\nsshd:!::0:::::\ngames:!::0:::::\nntp:!::0:::::\nguest:!::0:::::\nnobody:!::0:::::\nagentos:!:20626:0:99999:7:::\n" - }, - { - "path": "/etc/shells", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "# valid login shells\n/bin/sh\n/bin/ash\n" - }, - { - "path": "/etc/ssl", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/ssl1.1", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/sysctl.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/udhcpc", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/home", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/home/agentos", - "type": "directory", - "mode": "2755", - "uid": 1000, - "gid": 1000 - }, - { - "path": "/lib", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/lib/apk", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/lib/firmware", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/lib/modules-load.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/lib/sysctl.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/media", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/media/cdrom", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/media/floppy", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/media/usb", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/mnt", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/opt", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/root", - "type": "directory", - "mode": "700", - "uid": 0, - "gid": 0 - }, - { - "path": "/run", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/run/lock", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/sbin", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/srv", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/sys", - "type": "directory", - "mode": "555", - "uid": 0, - "gid": 0 - }, - { - "path": "/tmp", - "type": "directory", - "mode": "1777", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/bin", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/bin/env", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "/bin/busybox" - }, - { - "path": "/usr/lib", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/lib/os-release", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "NAME=\"Alpine Linux\"\nID=alpine\nVERSION_ID=3.22.4\nPRETTY_NAME=\"Alpine Linux v3.22\"\nHOME_URL=\"https://alpinelinux.org/\"\nBUG_REPORT_URL=\"https://gitlab.alpinelinux.org/alpine/aports/-/issues\"\n" - }, - { - "path": "/usr/local", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/sbin", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/share", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/cache", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/empty", - "type": "directory", - "mode": "555", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/lib", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/local", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/lock", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "../run/lock" - }, - { - "path": "/var/log", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/mail", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/opt", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/run", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "../run" - }, - { - "path": "/var/spool", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/spool/cron", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/spool/cron/crontabs", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "../../../etc/crontabs" - }, - { - "path": "/var/tmp", - "type": "directory", - "mode": "1777", - "uid": 0, - "gid": 0 - } - ] - } -} diff --git a/packages/core/fixtures/base-filesystem.json b/packages/core/fixtures/base-filesystem.json deleted file mode 100644 index 64e03bfa5..000000000 --- a/packages/core/fixtures/base-filesystem.json +++ /dev/null @@ -1,536 +0,0 @@ -{ - "source": { - "snapshotPath": "alpine-defaults.json", - "image": "alpine:3.22", - "snapshotCreatedAt": "2026-06-22T19:45:20.529Z", - "builtAt": "2026-06-23T03:47:12.644Z", - "transforms": [ - "Normalize HOSTNAME to secure-exec", - "Preserve the captured user-level environment and filesystem layout as the secure-exec base layer", - "Add the non-Alpine /workspace directory (default agent working directory) owned by the base user" - ] - }, - "environment": { - "env": { - "CHARSET": "UTF-8", - "HOME": "/home/agentos", - "HOSTNAME": "secure-exec", - "LANG": "C.UTF-8", - "LC_COLLATE": "C", - "LOGNAME": "agentos", - "PAGER": "less", - "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", - "SHELL": "/bin/sh", - "USER": "agentos" - }, - "prompt": "\\h:\\w\\$ " - }, - "filesystem": { - "entries": [ - { - "path": "/", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/bin", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/alpine-release", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "3.22.4\n" - }, - { - "path": "/etc/apk", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/busybox-paths.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/crontabs", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/group", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "root:x:0:root\nbin:x:1:root,bin,daemon\ndaemon:x:2:root,bin,daemon\nsys:x:3:root,bin\nadm:x:4:root,daemon\ntty:x:5:\ndisk:x:6:root\nlp:x:7:lp\nkmem:x:9:\nwheel:x:10:root\nfloppy:x:11:root\nmail:x:12:mail\nnews:x:13:news\nuucp:x:14:uucp\ncron:x:16:cron\naudio:x:18:\ncdrom:x:19:\ndialout:x:20:root\nftp:x:21:\nsshd:x:22:\ninput:x:23:\ntape:x:26:root\nvideo:x:27:root\nnetdev:x:28:\nkvm:x:34:kvm\ngames:x:35:\nshadow:x:42:\nwww-data:x:82:\nusers:x:100:games\nntp:x:123:\nabuild:x:300:\nutmp:x:406:\nping:x:999:\nnogroup:x:65533:\nnobody:x:65534:\nagentos:x:1000:\n" - }, - { - "path": "/etc/hostname", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "secure-exec\n" - }, - { - "path": "/etc/logrotate.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/modprobe.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/modules-load.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/network", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/nsswitch.conf", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "# musl itself does not support NSS, however some third-party DNS\n# implementations use the nsswitch.conf file to determine what\n# policy to follow.\n# Editing this file is not recommended.\nhosts: files dns\n" - }, - { - "path": "/etc/opt", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/os-release", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "../usr/lib/os-release" - }, - { - "path": "/etc/passwd", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "root:x:0:0:root:/root:/bin/sh\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaemon:x:2:2:daemon:/sbin:/sbin/nologin\nlp:x:4:7:lp:/var/spool/lpd:/sbin/nologin\nsync:x:5:0:sync:/sbin:/bin/sync\nshutdown:x:6:0:shutdown:/sbin:/sbin/shutdown\nhalt:x:7:0:halt:/sbin:/sbin/halt\nmail:x:8:12:mail:/var/mail:/sbin/nologin\nnews:x:9:13:news:/usr/lib/news:/sbin/nologin\nuucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin\ncron:x:16:16:cron:/var/spool/cron:/sbin/nologin\nftp:x:21:21::/var/lib/ftp:/sbin/nologin\nsshd:x:22:22:sshd:/dev/null:/sbin/nologin\ngames:x:35:35:games:/usr/games:/sbin/nologin\nntp:x:123:123:NTP:/var/empty:/sbin/nologin\nguest:x:405:100:guest:/dev/null:/sbin/nologin\nnobody:x:65534:65534:nobody:/:/sbin/nologin\nagentos:x:1000:1000::/home/agentos:/bin/sh\n" - }, - { - "path": "/etc/periodic", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/profile", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "export PATH=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"\n\nexport PAGER=less\numask 022\n\n# use nicer PS1 for bash and busybox ash\nif [ -n \"$BASH_VERSION\" -o \"$BB_ASH_VERSION\" ]; then\n\tPS1='\\h:\\w\\$ '\n# use nicer PS1 for zsh\nelif [ -n \"$ZSH_VERSION\" ]; then\n\tPS1='%m:%~%# '\n# set up fallback default PS1\nelse\n\t: \"${HOSTNAME:=$(hostname)}\"\n\tPS1='${HOSTNAME%%.*}:$PWD'\n\t[ \"$(id -u)\" -eq 0 ] && PS1=\"${PS1}# \" || PS1=\"${PS1}\\$ \"\nfi\n\nfor script in /etc/profile.d/*.sh ; do\n\tif [ -r \"$script\" ] ; then\n\t\t. \"$script\"\n\tfi\ndone\nunset script\n" - }, - { - "path": "/etc/profile.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/secfixes.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/shadow", - "type": "file", - "mode": "640", - "uid": 0, - "gid": 42, - "content": "root:*::0:::::\nbin:!::0:::::\ndaemon:!::0:::::\nlp:!::0:::::\nsync:!::0:::::\nshutdown:!::0:::::\nhalt:!::0:::::\nmail:!::0:::::\nnews:!::0:::::\nuucp:!::0:::::\ncron:!::0:::::\nftp:!::0:::::\nsshd:!::0:::::\ngames:!::0:::::\nntp:!::0:::::\nguest:!::0:::::\nnobody:!::0:::::\nagentos:!:20626:0:99999:7:::\n" - }, - { - "path": "/etc/shells", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "# valid login shells\n/bin/sh\n/bin/ash\n" - }, - { - "path": "/etc/ssl", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/ssl1.1", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/sysctl.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/etc/udhcpc", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/home", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/home/agentos", - "type": "directory", - "mode": "2755", - "uid": 1000, - "gid": 1000 - }, - { - "path": "/lib", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/lib/apk", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/lib/firmware", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/lib/modules-load.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/lib/sysctl.d", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/media", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/media/cdrom", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/media/floppy", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/media/usb", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/mnt", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/opt", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/root", - "type": "directory", - "mode": "700", - "uid": 0, - "gid": 0 - }, - { - "path": "/run", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/run/lock", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/sbin", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/srv", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/sys", - "type": "directory", - "mode": "555", - "uid": 0, - "gid": 0 - }, - { - "path": "/tmp", - "type": "directory", - "mode": "1777", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/bin", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/bin/env", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "/bin/busybox" - }, - { - "path": "/usr/lib", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/lib/os-release", - "type": "file", - "mode": "644", - "uid": 0, - "gid": 0, - "content": "NAME=\"Alpine Linux\"\nID=alpine\nVERSION_ID=3.22.4\nPRETTY_NAME=\"Alpine Linux v3.22\"\nHOME_URL=\"https://alpinelinux.org/\"\nBUG_REPORT_URL=\"https://gitlab.alpinelinux.org/alpine/aports/-/issues\"\n" - }, - { - "path": "/usr/local", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/sbin", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/usr/share", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/cache", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/empty", - "type": "directory", - "mode": "555", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/lib", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/local", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/lock", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "../run/lock" - }, - { - "path": "/var/log", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/mail", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/opt", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/run", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "../run" - }, - { - "path": "/var/spool", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/spool/cron", - "type": "directory", - "mode": "755", - "uid": 0, - "gid": 0 - }, - { - "path": "/var/spool/cron/crontabs", - "type": "symlink", - "mode": "777", - "uid": 0, - "gid": 0, - "target": "../../../etc/crontabs" - }, - { - "path": "/var/tmp", - "type": "directory", - "mode": "1777", - "uid": 0, - "gid": 0 - }, - { - "path": "/workspace", - "type": "directory", - "mode": "755", - "uid": 1000, - "gid": 1000 - } - ] - } -} diff --git a/packages/core/package.json b/packages/core/package.json index b515fcc38..30e43d64c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -7,7 +7,6 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "fixtures", "commands", "README.md" ], @@ -171,9 +170,6 @@ "types": "./dist/correlation.d.ts", "import": "./dist/correlation.js", "default": "./dist/correlation.js" - }, - "./fixtures/base-filesystem.json": { - "default": "./fixtures/base-filesystem.json" } }, "scripts": { diff --git a/packages/core/src/descriptors.ts b/packages/core/src/descriptors.ts index 736f57c95..bad0ba9a3 100644 --- a/packages/core/src/descriptors.ts +++ b/packages/core/src/descriptors.ts @@ -112,6 +112,12 @@ export interface LiveProjectedModuleDescriptor { entrypoint: string; } +export interface LivePackageDescriptor { + name: string; + dir: string; + acp_entrypoint?: string; +} + export function toGeneratedSidecarPlacement( placement: LiveSidecarPlacement, ): protocol.SidecarPlacement { @@ -162,3 +168,13 @@ export function toGeneratedProjectedModuleDescriptor( entrypoint: descriptor.entrypoint, }; } + +export function toGeneratedPackageDescriptor( + descriptor: LivePackageDescriptor, +): protocol.PackageDescriptor { + return { + name: descriptor.name, + dir: descriptor.dir, + acpEntrypoint: descriptor.acp_entrypoint ?? null, + }; +} diff --git a/packages/core/src/generated-protocol.ts b/packages/core/src/generated-protocol.ts index b3c7540c6..62fc0e457 100644 --- a/packages/core/src/generated-protocol.ts +++ b/packages/core/src/generated-protocol.ts @@ -1131,6 +1131,32 @@ export function writeWasmPermissionTier(bc: bare.ByteCursor, x: WasmPermissionTi } } +/** + * agentOS package descriptor. The sidecar projects the self-contained `dir` + * read-only under `//` and links its `bin/` + * commands onto $PATH. `dir` is a trusted host path (the client configures its + * own VM); the sidecar is the host-side TCB that builds the staging tree. + */ +export type PackageDescriptor = { + readonly name: string + readonly dir: string + readonly acpEntrypoint: string | null +} + +export function readPackageDescriptor(bc: bare.ByteCursor): PackageDescriptor { + return { + name: bare.readString(bc), + dir: bare.readString(bc), + acpEntrypoint: read0(bc), + } +} + +export function writePackageDescriptor(bc: bare.ByteCursor, x: PackageDescriptor): void { + bare.writeString(bc, x.name) + bare.writeString(bc, x.dir) + write0(bc, x.acpEntrypoint) +} + function read12(bc: bare.ByteCursor): readonly MountDescriptor[] { const len = bare.readUintSafe(bc) if (len === 0) { @@ -1222,6 +1248,25 @@ function write16(bc: bare.ByteCursor, x: ReadonlyMap } } +function read17(bc: bare.ByteCursor): readonly PackageDescriptor[] { + const len = bare.readUintSafe(bc) + if (len === 0) { + return [] + } + const result = [readPackageDescriptor(bc)] + for (let i = 1; i < len; i++) { + result[i] = readPackageDescriptor(bc) + } + return result +} + +function write17(bc: bare.ByteCursor, x: readonly PackageDescriptor[]): void { + bare.writeUintSafe(bc, x.length) + for (let i = 0; i < x.length; i++) { + writePackageDescriptor(bc, x[i]) + } +} + export type ConfigureVmRequest = { readonly mounts: readonly MountDescriptor[] readonly software: readonly SoftwareDescriptor[] @@ -1231,6 +1276,8 @@ export type ConfigureVmRequest = { readonly projectedModules: readonly ProjectedModuleDescriptor[] readonly commandPermissions: ReadonlyMap readonly loopbackExemptPorts: Uint16Array + readonly packages: readonly PackageDescriptor[] + readonly packagesMountAt: string } export function readConfigureVmRequest(bc: bare.ByteCursor): ConfigureVmRequest { @@ -1243,6 +1290,8 @@ export function readConfigureVmRequest(bc: bare.ByteCursor): ConfigureVmRequest projectedModules: read15(bc), commandPermissions: read16(bc), loopbackExemptPorts: bare.readU16Array(bc), + packages: read17(bc), + packagesMountAt: bare.readString(bc), } } @@ -1255,6 +1304,22 @@ export function writeConfigureVmRequest(bc: bare.ByteCursor, x: ConfigureVmReque write15(bc, x.projectedModules) write16(bc, x.commandPermissions) bare.writeU16Array(bc, x.loopbackExemptPorts) + write17(bc, x.packages) + bare.writeString(bc, x.packagesMountAt) +} + +export type LinkPackageRequest = { + readonly package: PackageDescriptor +} + +export function readLinkPackageRequest(bc: bare.ByteCursor): LinkPackageRequest { + return { + package: readPackageDescriptor(bc), + } +} + +export function writeLinkPackageRequest(bc: bare.ByteCursor, x: LinkPackageRequest): void { + writePackageDescriptor(bc, x.package) } export type RegisteredHostCallbackExample = { @@ -1274,18 +1339,18 @@ export function writeRegisteredHostCallbackExample(bc: bare.ByteCursor, x: Regis writeJsonUtf8(bc, x.input) } -function read17(bc: bare.ByteCursor): u64 | null { +function read18(bc: bare.ByteCursor): u64 | null { return bare.readBool(bc) ? bare.readU64(bc) : null } -function write17(bc: bare.ByteCursor, x: u64 | null): void { +function write18(bc: bare.ByteCursor, x: u64 | null): void { bare.writeBool(bc, x != null) if (x != null) { bare.writeU64(bc, x) } } -function read18(bc: bare.ByteCursor): readonly RegisteredHostCallbackExample[] { +function read19(bc: bare.ByteCursor): readonly RegisteredHostCallbackExample[] { const len = bare.readUintSafe(bc) if (len === 0) { return [] @@ -1297,7 +1362,7 @@ function read18(bc: bare.ByteCursor): readonly RegisteredHostCallbackExample[] { return result } -function write18(bc: bare.ByteCursor, x: readonly RegisteredHostCallbackExample[]): void { +function write19(bc: bare.ByteCursor, x: readonly RegisteredHostCallbackExample[]): void { bare.writeUintSafe(bc, x.length) for (let i = 0; i < x.length; i++) { writeRegisteredHostCallbackExample(bc, x[i]) @@ -1315,19 +1380,19 @@ export function readRegisteredHostCallbackDefinition(bc: bare.ByteCursor): Regis return { description: bare.readString(bc), inputSchema: readJsonUtf8(bc), - timeoutMs: read17(bc), - examples: read18(bc), + timeoutMs: read18(bc), + examples: read19(bc), } } export function writeRegisteredHostCallbackDefinition(bc: bare.ByteCursor, x: RegisteredHostCallbackDefinition): void { bare.writeString(bc, x.description) writeJsonUtf8(bc, x.inputSchema) - write17(bc, x.timeoutMs) - write18(bc, x.examples) + write18(bc, x.timeoutMs) + write19(bc, x.examples) } -function read19(bc: bare.ByteCursor): ReadonlyMap { +function read20(bc: bare.ByteCursor): ReadonlyMap { const len = bare.readUintSafe(bc) const result = new Map() for (let i = 0; i < len; i++) { @@ -1342,7 +1407,7 @@ function read19(bc: bare.ByteCursor): ReadonlyMap): void { +function write20(bc: bare.ByteCursor, x: ReadonlyMap): void { bare.writeUintSafe(bc, x.size) for (const kv of x) { bare.writeString(bc, kv[0]) @@ -1364,7 +1429,7 @@ export function readRegisterHostCallbacksRequest(bc: bare.ByteCursor): RegisterH description: bare.readString(bc), commandAliases: read6(bc), registryCommandAliases: read6(bc), - callbacks: read19(bc), + callbacks: read20(bc), } } @@ -1373,7 +1438,7 @@ export function writeRegisterHostCallbacksRequest(bc: bare.ByteCursor, x: Regist bare.writeString(bc, x.description) write6(bc, x.commandAliases) write6(bc, x.registryCommandAliases) - write19(bc, x.callbacks) + write20(bc, x.callbacks) } export type CreateLayerRequest = null @@ -1628,10 +1693,10 @@ export function readGuestFilesystemCallRequest(bc: bare.ByteCursor): GuestFilesy mode: read2(bc), uid: read2(bc), gid: read2(bc), - atimeMs: read17(bc), - mtimeMs: read17(bc), - len: read17(bc), - offset: read17(bc), + atimeMs: read18(bc), + mtimeMs: read18(bc), + len: read18(bc), + offset: read18(bc), } } @@ -1646,30 +1711,30 @@ export function writeGuestFilesystemCallRequest(bc: bare.ByteCursor, x: GuestFil write2(bc, x.mode) write2(bc, x.uid) write2(bc, x.gid) - write17(bc, x.atimeMs) - write17(bc, x.mtimeMs) - write17(bc, x.len) - write17(bc, x.offset) + write18(bc, x.atimeMs) + write18(bc, x.mtimeMs) + write18(bc, x.len) + write18(bc, x.offset) } export type SnapshotRootFilesystemRequest = null -function read20(bc: bare.ByteCursor): GuestRuntimeKind | null { +function read21(bc: bare.ByteCursor): GuestRuntimeKind | null { return bare.readBool(bc) ? readGuestRuntimeKind(bc) : null } -function write20(bc: bare.ByteCursor, x: GuestRuntimeKind | null): void { +function write21(bc: bare.ByteCursor, x: GuestRuntimeKind | null): void { bare.writeBool(bc, x != null) if (x != null) { writeGuestRuntimeKind(bc, x) } } -function read21(bc: bare.ByteCursor): WasmPermissionTier | null { +function read22(bc: bare.ByteCursor): WasmPermissionTier | null { return bare.readBool(bc) ? readWasmPermissionTier(bc) : null } -function write21(bc: bare.ByteCursor, x: WasmPermissionTier | null): void { +function write22(bc: bare.ByteCursor, x: WasmPermissionTier | null): void { bare.writeBool(bc, x != null) if (x != null) { writeWasmPermissionTier(bc, x) @@ -1691,24 +1756,24 @@ export function readExecuteRequest(bc: bare.ByteCursor): ExecuteRequest { return { processId: bare.readString(bc), command: read0(bc), - runtime: read20(bc), + runtime: read21(bc), entrypoint: read0(bc), args: read6(bc), env: read1(bc), cwd: read0(bc), - wasmPermissionTier: read21(bc), + wasmPermissionTier: read22(bc), } } export function writeExecuteRequest(bc: bare.ByteCursor, x: ExecuteRequest): void { bare.writeString(bc, x.processId) write0(bc, x.command) - write20(bc, x.runtime) + write21(bc, x.runtime) write0(bc, x.entrypoint) write6(bc, x.args) write1(bc, x.env) write0(bc, x.cwd) - write21(bc, x.wasmPermissionTier) + write22(bc, x.wasmPermissionTier) } export type WriteStdinRequest = { @@ -1781,11 +1846,11 @@ export function writeKillProcessRequest(bc: bare.ByteCursor, x: KillProcessReque export type GetProcessSnapshotRequest = null -function read22(bc: bare.ByteCursor): u16 | null { +function read23(bc: bare.ByteCursor): u16 | null { return bare.readBool(bc) ? bare.readU16(bc) : null } -function write22(bc: bare.ByteCursor, x: u16 | null): void { +function write23(bc: bare.ByteCursor, x: u16 | null): void { bare.writeBool(bc, x != null) if (x != null) { bare.writeU16(bc, x) @@ -1801,14 +1866,14 @@ export type FindListenerRequest = { export function readFindListenerRequest(bc: bare.ByteCursor): FindListenerRequest { return { host: read0(bc), - port: read22(bc), + port: read23(bc), path: read0(bc), } } export function writeFindListenerRequest(bc: bare.ByteCursor, x: FindListenerRequest): void { write0(bc, x.host) - write22(bc, x.port) + write23(bc, x.port) write0(bc, x.path) } @@ -1820,13 +1885,13 @@ export type FindBoundUdpRequest = { export function readFindBoundUdpRequest(bc: bare.ByteCursor): FindBoundUdpRequest { return { host: read0(bc), - port: read22(bc), + port: read23(bc), } } export function writeFindBoundUdpRequest(bc: bare.ByteCursor, x: FindBoundUdpRequest): void { write0(bc, x.host) - write22(bc, x.port) + write23(bc, x.port) } export type GetSignalStateRequest = { @@ -2020,6 +2085,7 @@ export type RequestPayload = | { readonly tag: "PersistenceFlushRequest"; readonly val: PersistenceFlushRequest } | { readonly tag: "VmFetchRequest"; readonly val: VmFetchRequest } | { readonly tag: "ExtEnvelope"; readonly val: ExtEnvelope } + | { readonly tag: "LinkPackageRequest"; readonly val: LinkPackageRequest } export function readRequestPayload(bc: bare.ByteCursor): RequestPayload { const offset = bc.offset @@ -2083,6 +2149,8 @@ export function readRequestPayload(bc: bare.ByteCursor): RequestPayload { return { tag: "VmFetchRequest", val: readVmFetchRequest(bc) } case 28: return { tag: "ExtEnvelope", val: readExtEnvelope(bc) } + case 29: + return { tag: "LinkPackageRequest", val: readLinkPackageRequest(bc) } default: { bc.offset = offset throw new bare.BareError(offset, "invalid tag") @@ -2233,6 +2301,11 @@ export function writeRequestPayload(bc: bare.ByteCursor, x: RequestPayload): voi writeExtEnvelope(bc, x.val) break } + case "LinkPackageRequest": { + bare.writeU8(bc, 29) + writeLinkPackageRequest(bc, x.val) + break + } } } @@ -2355,6 +2428,20 @@ export function writeVmConfiguredResponse(bc: bare.ByteCursor, x: VmConfiguredRe bare.writeU32(bc, x.appliedSoftware) } +export type PackageLinkedResponse = { + readonly commands: readonly string[] +} + +export function readPackageLinkedResponse(bc: bare.ByteCursor): PackageLinkedResponse { + return { + commands: read6(bc), + } +} + +export function writePackageLinkedResponse(bc: bare.ByteCursor, x: PackageLinkedResponse): void { + write6(bc, x.commands) +} + export type HostCallbacksRegisteredResponse = { readonly registration: string readonly commandCount: u32 @@ -2501,33 +2588,33 @@ export function writeGuestFilesystemStat(bc: bare.ByteCursor, x: GuestFilesystem bare.writeU32(bc, x.gid) } -function read23(bc: bare.ByteCursor): readonly string[] | null { +function read24(bc: bare.ByteCursor): readonly string[] | null { return bare.readBool(bc) ? read6(bc) : null } -function write23(bc: bare.ByteCursor, x: readonly string[] | null): void { +function write24(bc: bare.ByteCursor, x: readonly string[] | null): void { bare.writeBool(bc, x != null) if (x != null) { write6(bc, x) } } -function read24(bc: bare.ByteCursor): GuestFilesystemStat | null { +function read25(bc: bare.ByteCursor): GuestFilesystemStat | null { return bare.readBool(bc) ? readGuestFilesystemStat(bc) : null } -function write24(bc: bare.ByteCursor, x: GuestFilesystemStat | null): void { +function write25(bc: bare.ByteCursor, x: GuestFilesystemStat | null): void { bare.writeBool(bc, x != null) if (x != null) { writeGuestFilesystemStat(bc, x) } } -function read25(bc: bare.ByteCursor): boolean | null { +function read26(bc: bare.ByteCursor): boolean | null { return bare.readBool(bc) ? bare.readBool(bc) : null } -function write25(bc: bare.ByteCursor, x: boolean | null): void { +function write26(bc: bare.ByteCursor, x: boolean | null): void { bare.writeBool(bc, x != null) if (x != null) { bare.writeBool(bc, x) @@ -2551,9 +2638,9 @@ export function readGuestFilesystemResultResponse(bc: bare.ByteCursor): GuestFil path: bare.readString(bc), content: read0(bc), encoding: read3(bc), - entries: read23(bc), - stat: read24(bc), - exists: read25(bc), + entries: read24(bc), + stat: read25(bc), + exists: read26(bc), target: read0(bc), } } @@ -2563,9 +2650,9 @@ export function writeGuestFilesystemResultResponse(bc: bare.ByteCursor, x: Guest bare.writeString(bc, x.path) write0(bc, x.content) write3(bc, x.encoding) - write23(bc, x.entries) - write24(bc, x.stat) - write25(bc, x.exists) + write24(bc, x.entries) + write25(bc, x.stat) + write26(bc, x.exists) write0(bc, x.target) } @@ -2705,11 +2792,11 @@ export function writeProcessSnapshotStatus(bc: bare.ByteCursor, x: ProcessSnapsh } } -function read26(bc: bare.ByteCursor): i32 | null { +function read27(bc: bare.ByteCursor): i32 | null { return bare.readBool(bc) ? bare.readI32(bc) : null } -function write26(bc: bare.ByteCursor, x: i32 | null): void { +function write27(bc: bare.ByteCursor, x: i32 | null): void { bare.writeBool(bc, x != null) if (x != null) { bare.writeI32(bc, x) @@ -2742,7 +2829,7 @@ export function readProcessSnapshotEntry(bc: bare.ByteCursor): ProcessSnapshotEn args: read6(bc), cwd: bare.readString(bc), status: readProcessSnapshotStatus(bc), - exitCode: read26(bc), + exitCode: read27(bc), } } @@ -2757,10 +2844,10 @@ export function writeProcessSnapshotEntry(bc: bare.ByteCursor, x: ProcessSnapsho write6(bc, x.args) bare.writeString(bc, x.cwd) writeProcessSnapshotStatus(bc, x.status) - write26(bc, x.exitCode) + write27(bc, x.exitCode) } -function read27(bc: bare.ByteCursor): readonly ProcessSnapshotEntry[] { +function read28(bc: bare.ByteCursor): readonly ProcessSnapshotEntry[] { const len = bare.readUintSafe(bc) if (len === 0) { return [] @@ -2772,7 +2859,7 @@ function read27(bc: bare.ByteCursor): readonly ProcessSnapshotEntry[] { return result } -function write27(bc: bare.ByteCursor, x: readonly ProcessSnapshotEntry[]): void { +function write28(bc: bare.ByteCursor, x: readonly ProcessSnapshotEntry[]): void { bare.writeUintSafe(bc, x.length) for (let i = 0; i < x.length; i++) { writeProcessSnapshotEntry(bc, x[i]) @@ -2785,12 +2872,12 @@ export type ProcessSnapshotResponse = { export function readProcessSnapshotResponse(bc: bare.ByteCursor): ProcessSnapshotResponse { return { - processes: read27(bc), + processes: read28(bc), } } export function writeProcessSnapshotResponse(bc: bare.ByteCursor, x: ProcessSnapshotResponse): void { - write27(bc, x.processes) + write28(bc, x.processes) } export type SocketStateEntry = { @@ -2804,7 +2891,7 @@ export function readSocketStateEntry(bc: bare.ByteCursor): SocketStateEntry { return { processId: bare.readString(bc), host: read0(bc), - port: read22(bc), + port: read23(bc), path: read0(bc), } } @@ -2812,15 +2899,15 @@ export function readSocketStateEntry(bc: bare.ByteCursor): SocketStateEntry { export function writeSocketStateEntry(bc: bare.ByteCursor, x: SocketStateEntry): void { bare.writeString(bc, x.processId) write0(bc, x.host) - write22(bc, x.port) + write23(bc, x.port) write0(bc, x.path) } -function read28(bc: bare.ByteCursor): SocketStateEntry | null { +function read29(bc: bare.ByteCursor): SocketStateEntry | null { return bare.readBool(bc) ? readSocketStateEntry(bc) : null } -function write28(bc: bare.ByteCursor, x: SocketStateEntry | null): void { +function write29(bc: bare.ByteCursor, x: SocketStateEntry | null): void { bare.writeBool(bc, x != null) if (x != null) { writeSocketStateEntry(bc, x) @@ -2833,12 +2920,12 @@ export type ListenerSnapshotResponse = { export function readListenerSnapshotResponse(bc: bare.ByteCursor): ListenerSnapshotResponse { return { - listener: read28(bc), + listener: read29(bc), } } export function writeListenerSnapshotResponse(bc: bare.ByteCursor, x: ListenerSnapshotResponse): void { - write28(bc, x.listener) + write29(bc, x.listener) } export type BoundUdpSnapshotResponse = { @@ -2847,12 +2934,12 @@ export type BoundUdpSnapshotResponse = { export function readBoundUdpSnapshotResponse(bc: bare.ByteCursor): BoundUdpSnapshotResponse { return { - socket: read28(bc), + socket: read29(bc), } } export function writeBoundUdpSnapshotResponse(bc: bare.ByteCursor, x: BoundUdpSnapshotResponse): void { - write28(bc, x.socket) + write29(bc, x.socket) } export enum SignalDispositionAction { @@ -2915,7 +3002,7 @@ export function writeSignalHandlerRegistration(bc: bare.ByteCursor, x: SignalHan bare.writeU32(bc, x.flags) } -function read29(bc: bare.ByteCursor): ReadonlyMap { +function read30(bc: bare.ByteCursor): ReadonlyMap { const len = bare.readUintSafe(bc) const result = new Map() for (let i = 0; i < len; i++) { @@ -2930,7 +3017,7 @@ function read29(bc: bare.ByteCursor): ReadonlyMap): void { +function write30(bc: bare.ByteCursor, x: ReadonlyMap): void { bare.writeUintSafe(bc, x.size) for (const kv of x) { bare.writeU32(bc, kv[0]) @@ -2946,13 +3033,13 @@ export type SignalStateResponse = { export function readSignalStateResponse(bc: bare.ByteCursor): SignalStateResponse { return { processId: bare.readString(bc), - handlers: read29(bc), + handlers: read30(bc), } } export function writeSignalStateResponse(bc: bare.ByteCursor, x: SignalStateResponse): void { bare.writeString(bc, x.processId) - write29(bc, x.handlers) + write30(bc, x.handlers) } export type ZombieTimerCountResponse = { @@ -3106,6 +3193,7 @@ export type ResponsePayload = | { readonly tag: "RejectedResponse"; readonly val: RejectedResponse } | { readonly tag: "VmFetchResponse"; readonly val: VmFetchResponse } | { readonly tag: "ExtEnvelope"; readonly val: ExtEnvelope } + | { readonly tag: "PackageLinkedResponse"; readonly val: PackageLinkedResponse } export function readResponsePayload(bc: bare.ByteCursor): ResponsePayload { const offset = bc.offset @@ -3173,6 +3261,8 @@ export function readResponsePayload(bc: bare.ByteCursor): ResponsePayload { return { tag: "VmFetchResponse", val: readVmFetchResponse(bc) } case 30: return { tag: "ExtEnvelope", val: readExtEnvelope(bc) } + case 31: + return { tag: "PackageLinkedResponse", val: readPackageLinkedResponse(bc) } default: { bc.offset = offset throw new bare.BareError(offset, "invalid tag") @@ -3337,6 +3427,11 @@ export function writeResponsePayload(bc: bare.ByteCursor, x: ResponsePayload): v writeExtEnvelope(bc, x.val) break } + case "PackageLinkedResponse": { + bare.writeU8(bc, 31) + writePackageLinkedResponse(bc, x.val) + break + } } } @@ -3707,11 +3802,11 @@ export function writeSidecarRequestFrame(bc: bare.ByteCursor, x: SidecarRequestF writeSidecarRequestPayload(bc, x.payload) } -function read30(bc: bare.ByteCursor): JsonUtf8 | null { +function read31(bc: bare.ByteCursor): JsonUtf8 | null { return bare.readBool(bc) ? readJsonUtf8(bc) : null } -function write30(bc: bare.ByteCursor, x: JsonUtf8 | null): void { +function write31(bc: bare.ByteCursor, x: JsonUtf8 | null): void { bare.writeBool(bc, x != null) if (x != null) { writeJsonUtf8(bc, x) @@ -3727,14 +3822,14 @@ export type HostCallbackResultResponse = { export function readHostCallbackResultResponse(bc: bare.ByteCursor): HostCallbackResultResponse { return { invocationId: bare.readString(bc), - result: read30(bc), + result: read31(bc), error: read0(bc), } } export function writeHostCallbackResultResponse(bc: bare.ByteCursor, x: HostCallbackResultResponse): void { bare.writeString(bc, x.invocationId) - write30(bc, x.result) + write31(bc, x.result) write0(bc, x.error) } @@ -3747,14 +3842,14 @@ export type JsBridgeResultResponse = { export function readJsBridgeResultResponse(bc: bare.ByteCursor): JsBridgeResultResponse { return { callId: bare.readString(bc), - result: read30(bc), + result: read31(bc), error: read0(bc), } } export function writeJsBridgeResultResponse(bc: bare.ByteCursor, x: JsBridgeResultResponse): void { bare.writeString(bc, x.callId) - write30(bc, x.result) + write31(bc, x.result) write0(bc, x.error) } diff --git a/packages/core/src/request-payloads.ts b/packages/core/src/request-payloads.ts index b3c7af339..1dcf944b0 100644 --- a/packages/core/src/request-payloads.ts +++ b/packages/core/src/request-payloads.ts @@ -1,10 +1,12 @@ import { toExactArrayBuffer } from "./bytes.js"; import { type LiveMountDescriptor, + type LivePackageDescriptor, type LiveProjectedModuleDescriptor, type LiveSidecarPlacement, type LiveSoftwareDescriptor, toGeneratedMountDescriptor, + toGeneratedPackageDescriptor, toGeneratedProjectedModuleDescriptor, toGeneratedSidecarPlacement, toGeneratedSoftwareDescriptor, @@ -78,6 +80,12 @@ export type LiveRequestPayload = projected_modules: LiveProjectedModuleDescriptor[]; command_permissions: Record; loopback_exempt_ports?: number[]; + packages?: LivePackageDescriptor[]; + packages_mount_at?: string; + } + | { + type: "link_package"; + package: LivePackageDescriptor; } | { type: "register_host_callbacks"; @@ -276,6 +284,17 @@ export function toGeneratedRequestPayload( loopbackExemptPorts: new Uint16Array( payload.loopback_exempt_ports ?? [], ), + packages: (payload.packages ?? []).map( + toGeneratedPackageDescriptor, + ), + packagesMountAt: payload.packages_mount_at ?? "", + }, + }; + case "link_package": + return { + tag: "LinkPackageRequest", + val: { + package: toGeneratedPackageDescriptor(payload.package), }, }; case "register_host_callbacks": diff --git a/packages/core/src/response-payloads.ts b/packages/core/src/response-payloads.ts index a145576b7..f6f6467d4 100644 --- a/packages/core/src/response-payloads.ts +++ b/packages/core/src/response-payloads.ts @@ -51,6 +51,10 @@ export type LiveResponsePayload = applied_mounts: number; applied_software: number; } + | { + type: "package_linked"; + commands: string[]; + } | { type: "host_callbacks_registered"; registration: string; @@ -208,6 +212,11 @@ export function fromGeneratedResponsePayload( applied_mounts: payload.val.appliedMounts, applied_software: payload.val.appliedSoftware, }; + case "PackageLinkedResponse": + return { + type: "package_linked", + commands: [...payload.val.commands], + }; case "HostCallbacksRegisteredResponse": return { type: "host_callbacks_registered", diff --git a/packages/core/src/sidecar-process.ts b/packages/core/src/sidecar-process.ts index 1dd665984..3b00c1857 100644 --- a/packages/core/src/sidecar-process.ts +++ b/packages/core/src/sidecar-process.ts @@ -241,6 +241,12 @@ export interface SidecarProjectedModuleDescriptor { entrypoint: string; } +export interface SidecarPackageDescriptor { + name: string; + dir: string; + acpEntrypoint?: string; +} + export interface SidecarFilesystemResult { operation: LiveFilesystemOperation; status: string; @@ -402,6 +408,8 @@ export class SidecarProcess { projectedModules?: SidecarProjectedModuleDescriptor[]; commandPermissions?: Record; loopbackExemptPorts?: number[]; + packages?: SidecarPackageDescriptor[]; + packagesMountAt?: string; }, ): Promise { const response = await this.sendRequest({ @@ -425,6 +433,10 @@ export class SidecarProcess { ...(options.loopbackExemptPorts ? { loopback_exempt_ports: options.loopbackExemptPorts } : {}), + packages: (options.packages ?? []).map(toWirePackageDescriptor), + ...(options.packagesMountAt + ? { packages_mount_at: options.packagesMountAt } + : {}), }, }); if (response.payload.type !== "vm_configured") { @@ -434,6 +446,37 @@ export class SidecarProcess { } } + /** + * Runtime dynamic `linkSoftware`: project one package into the live + * `/opt/agentos` staging dir owned by the sidecar. Returns the linked command + * names. The host-dir mount reflects host writes, so the commands appear under + * `/opt/agentos/bin` immediately with no reboot. + */ + async linkPackage( + session: AuthenticatedSession, + vm: CreatedVm, + descriptor: SidecarPackageDescriptor, + ): Promise { + const response = await this.sendRequest({ + ownership: { + scope: "vm", + connection_id: session.connectionId, + session_id: session.sessionId, + vm_id: vm.vmId, + }, + payload: { + type: "link_package", + package: toWirePackageDescriptor(descriptor), + }, + }); + if (response.payload.type !== "package_linked") { + throw new Error( + `unexpected link_package response: ${response.payload.type}`, + ); + } + return response.payload.commands; + } + async registerHostCallbacks( session: AuthenticatedSession, vm: CreatedVm, @@ -1511,3 +1554,17 @@ function toWireProjectedModuleDescriptor( entrypoint: descriptor.entrypoint, }; } + +function toWirePackageDescriptor(descriptor: SidecarPackageDescriptor): { + name: string; + dir: string; + acp_entrypoint?: string; +} { + return { + name: descriptor.name, + dir: descriptor.dir, + ...(descriptor.acpEntrypoint !== undefined + ? { acp_entrypoint: descriptor.acpEntrypoint } + : {}), + }; +} diff --git a/packages/registry-types/package.json b/packages/manifest/package.json similarity index 90% rename from packages/registry-types/package.json rename to packages/manifest/package.json index 419f08a10..21491620d 100644 --- a/packages/registry-types/package.json +++ b/packages/manifest/package.json @@ -1,5 +1,5 @@ { - "name": "@secure-exec/registry-types", + "name": "@agentos-software/manifest", "version": "0.3.0-rc.1", "type": "module", "license": "Apache-2.0", diff --git a/packages/registry-types/src/index.ts b/packages/manifest/src/index.ts similarity index 63% rename from packages/registry-types/src/index.ts rename to packages/manifest/src/index.ts index 9269e23aa..b072bb8c6 100644 --- a/packages/registry-types/src/index.ts +++ b/packages/manifest/src/index.ts @@ -1,3 +1,34 @@ +/** + * Agent metadata for an agent package descriptor. + */ +export interface PackageAgentDescriptor { + /** package.json `bin` command that speaks ACP over stdio. */ + acpEntrypoint: string; + /** Static environment variables for the agent process. */ + env?: Record; + /** Optional extra launch arguments. */ + launchArgs?: string[]; + /** Optional snapshot flag. */ + snapshot?: boolean; +} + +/** + * Descriptor for a registry package (software or agent). + * + * Each @agentos-software/* package default-exports a plain object literal + * satisfying this type. Commands are derived by the sidecar from the package's + * package.json `bin` map (or a `bin/` directory of wasm binaries), so no + * per-command metadata lives here. + */ +export interface PackageDescriptor { + /** Short package name (e.g., "jq", "git", "claude"). */ + name: string; + /** Absolute path to the self-contained package directory. */ + dir: string; + /** Present only for agent packages. */ + agent?: PackageAgentDescriptor; +} + /** * Permission tier for WASM command execution. * Shared runtime permission tiers for registry command metadata. diff --git a/packages/registry-types/tsconfig.json b/packages/manifest/tsconfig.json similarity index 100% rename from packages/registry-types/tsconfig.json rename to packages/manifest/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7dcc166b5..f00aac7ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -332,6 +332,21 @@ importers: specifier: ^5.7.2 version: 5.9.3 + packages/agentos-toolchain: + devDependencies: + '@types/node': + specifier: ^22.10.2 + version: 22.19.15 + tsup: + specifier: ^8.3.5 + version: 8.5.1(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) + packages/benchmarks: dependencies: secure-exec: @@ -407,7 +422,7 @@ importers: specifier: ^2.1.8 version: 2.1.9(@types/node@22.19.15) - packages/registry-types: + packages/manifest: devDependencies: '@types/node': specifier: ^22.10.2 @@ -459,6 +474,9 @@ importers: specifier: ^4.1.11 version: 4.3.6 devDependencies: + '@agentos-software/manifest': + specifier: workspace:* + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -470,8 +488,11 @@ importers: dependencies: '@agentos-software/codex-cli': specifier: workspace:* - version: link:../../software/codex + version: link:../../software/codex-cli devDependencies: + '@agentos-software/manifest': + specifier: workspace:* + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -481,6 +502,9 @@ importers: registry/agent/opencode: devDependencies: + '@agentos-software/manifest': + specifier: workspace:* + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -503,6 +527,9 @@ importers: specifier: 0.60.0 version: 0.60.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.21.0)(zod@4.3.6) devDependencies: + '@agentos-software/manifest': + specifier: workspace:* + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -519,44 +546,15 @@ importers: specifier: ^0.0.23 version: 0.0.23 devDependencies: - '@types/node': - specifier: ^22.10.2 - version: 22.19.15 - typescript: - specifier: ^5.7.2 - version: 5.9.3 - - registry/file-system/google-drive: - dependencies: - '@secure-exec/core': - specifier: workspace:* - version: link:../../../packages/core - devDependencies: - '@types/node': - specifier: ^22.10.2 - version: 22.19.15 - typescript: - specifier: ^5.7.2 - version: 5.9.3 - vitest: - specifier: ^2.1.8 - version: 2.1.9(@types/node@22.19.15) - - registry/file-system/s3: - dependencies: - '@secure-exec/core': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/core - devDependencies: + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.7.2 version: 5.9.3 - vitest: - specifier: ^2.1.8 - version: 2.1.9(@types/node@22.19.15) registry/software/build-essential: dependencies: @@ -573,9 +571,9 @@ importers: specifier: workspace:* version: link:../make devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -583,11 +581,11 @@ importers: specifier: ^5.9.2 version: 5.9.3 - registry/software/codex: + registry/software/codex-cli: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -622,9 +620,9 @@ importers: specifier: workspace:* version: link:../tar devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -634,9 +632,9 @@ importers: registry/software/coreutils: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -646,9 +644,9 @@ importers: registry/software/curl: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -658,9 +656,9 @@ importers: registry/software/diffutils: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -670,9 +668,9 @@ importers: registry/software/duckdb: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -684,7 +682,7 @@ importers: dependencies: '@agentos-software/codex-cli': specifier: workspace:* - version: link:../codex + version: link:../codex-cli '@agentos-software/coreutils': specifier: workspace:* version: link:../coreutils @@ -737,9 +735,9 @@ importers: specifier: workspace:* version: link:../zip devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -749,9 +747,9 @@ importers: registry/software/fd: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -761,9 +759,9 @@ importers: registry/software/file: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -773,9 +771,9 @@ importers: registry/software/findutils: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -785,9 +783,9 @@ importers: registry/software/gawk: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -797,9 +795,9 @@ importers: registry/software/git: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -809,9 +807,9 @@ importers: registry/software/grep: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -821,9 +819,9 @@ importers: registry/software/gzip: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -833,9 +831,9 @@ importers: registry/software/http-get: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -845,9 +843,9 @@ importers: registry/software/jq: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -857,9 +855,9 @@ importers: registry/software/make: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -869,9 +867,9 @@ importers: registry/software/ripgrep: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -881,9 +879,9 @@ importers: registry/software/sed: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -893,9 +891,9 @@ importers: registry/software/sqlite3: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -905,9 +903,9 @@ importers: registry/software/tar: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -917,9 +915,9 @@ importers: registry/software/tree: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -929,9 +927,9 @@ importers: registry/software/unzip: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -941,9 +939,9 @@ importers: registry/software/wget: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -953,9 +951,9 @@ importers: registry/software/yq: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -965,9 +963,9 @@ importers: registry/software/zip: devDependencies: - '@secure-exec/registry-types': + '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/registry-types + version: link:../../../packages/manifest '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -4003,6 +4001,12 @@ packages: os: [darwin, linux, win32] hasBin: true + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -4069,6 +4073,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} @@ -4163,6 +4171,13 @@ packages: common-ancestor-path@1.0.1: resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + console-browserify@1.2.0: resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} @@ -4815,6 +4830,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flattie@1.1.1: resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} engines: {node: '>=8'} @@ -5317,6 +5335,10 @@ packages: react: optional: true + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5383,6 +5405,10 @@ packages: linkify-it@5.0.1: resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -5724,6 +5750,9 @@ packages: mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + motion-dom@11.18.1: resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==} @@ -5970,6 +5999,9 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} @@ -6020,6 +6052,9 @@ packages: resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} engines: {node: '>=10'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + playwright-core@1.59.1: resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} engines: {node: '>=18'} @@ -6314,6 +6349,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + readdirp@5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} @@ -6402,6 +6441,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -6822,6 +6865,10 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -6852,6 +6899,25 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -10706,6 +10772,11 @@ snapshots: '@oven/bun-windows-x64': 1.3.11 '@oven/bun-windows-x64-baseline': 1.3.11 + bundle-require@5.1.0(esbuild@0.27.4): + dependencies: + esbuild: 0.27.4 + load-tsconfig: 0.2.5 + bytes@3.1.2: {} cac@6.7.14: {} @@ -10772,6 +10843,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + chokidar@5.0.0: dependencies: readdirp: 5.0.0 @@ -10875,6 +10950,10 @@ snapshots: common-ancestor-path@1.0.1: {} + confbox@0.1.8: {} + + consola@3.4.2: {} + console-browserify@1.2.0: {} constants-browserify@1.0.0: {} @@ -11684,6 +11763,12 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.2 + rollup: 4.60.1 + flattie@1.1.1: {} follow-redirects@1.16.0: {} @@ -12252,6 +12337,8 @@ snapshots: '@types/react': 19.2.17 react: 19.2.7 + joycon@3.1.1: {} + js-tokens@4.0.0: {} js-yaml@4.2.0: @@ -12309,6 +12396,8 @@ snapshots: dependencies: uc.micro: 2.1.0 + load-tsconfig@0.2.5: {} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -12917,6 +13006,13 @@ snapshots: mkdirp-classic@0.5.3: optional: true + mlly@1.8.2: + dependencies: + acorn: 8.17.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + motion-dom@11.18.1: dependencies: motion-utils: 11.18.1 @@ -13186,6 +13282,8 @@ snapshots: pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@2.0.1: {} pbkdf2@3.1.5: @@ -13226,6 +13324,12 @@ snapshots: dependencies: find-up: 5.0.0 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + playwright-core@1.59.1: {} playwright@1.59.1: @@ -13561,6 +13665,8 @@ snapshots: dependencies: picomatch: 2.3.2 + readdirp@4.1.2: {} + readdirp@5.0.0: {} recharts-scale@0.4.5: @@ -13718,6 +13824,8 @@ snapshots: require-from-string@2.0.2: {} + resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} resolve@1.22.11: @@ -14280,6 +14388,8 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + tree-kill@1.2.2: {} + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -14296,6 +14406,34 @@ snapshots: tslib@2.8.1: {} + tsup@8.5.1(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.4) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.4 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.9.0) + resolve-from: 5.0.0 + rollup: 4.60.1 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.8 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsx@4.21.0: dependencies: esbuild: 0.27.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2f9322d9a..9bceee63f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,7 +5,6 @@ packages: - examples/docs/* - registry/agent/* - registry/software/* - - registry/file-system/* - registry/tool/* - scripts/publish diff --git a/registry/agent/claude/package.json b/registry/agent/claude/package.json index 71cc71306..faa38cbf8 100644 --- a/registry/agent/claude/package.json +++ b/registry/agent/claude/package.json @@ -15,8 +15,11 @@ "default": "./dist/index.js" } }, + "files": [ + "dist" + ], "scripts": { - "build": "tsc && node ./scripts/build-patched-cli.mjs", + "build": "tsc && node ./scripts/build-patched-cli.mjs && rm -rf dist/package && node ../../../packages/agentos-toolchain/dist/cli.js pack . --out dist/package --agent claude-sdk-acp --prune-native", "check-types": "tsc --noEmit", "test": "pnpm build && node --test --test-force-exit tests/*.test.mjs" }, @@ -26,6 +29,7 @@ "zod": "^4.1.11" }, "devDependencies": { + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.7.2" } diff --git a/registry/agent/claude/src/index.ts b/registry/agent/claude/src/index.ts index 4e99df50d..59b355348 100644 --- a/registry/agent/claude/src/index.ts +++ b/registry/agent/claude/src/index.ts @@ -1,19 +1,15 @@ import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import type { PackageDescriptor } from "@agentos-software/manifest"; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const packageDir = resolve(__dirname, ".."); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const claude = { +export default { name: "claude", - type: "agent" as const, - packageDir, - requires: ["@agentclientprotocol/sdk", "@anthropic-ai/claude-agent-sdk"], + dir, agent: { - id: "claude", - acpAdapter: "@agentos-software/claude-code", - agentPackage: "@anthropic-ai/claude-agent-sdk", - staticEnv: { + acpEntrypoint: "claude-sdk-acp", + env: { CLAUDE_AGENT_SDK_CLIENT_APP: "@rivet-dev/agentos", CLAUDE_CODE_SIMPLE: "1", CLAUDE_CODE_FORCE_AGENT_OS_RIPGREP: "1", @@ -33,6 +29,4 @@ const claude = { USE_BUILTIN_RIPGREP: "0", }, }, -}; - -export default claude; +} satisfies PackageDescriptor; diff --git a/registry/agent/codex/package.json b/registry/agent/codex/package.json index 40bd4aded..bdc2e759a 100644 --- a/registry/agent/codex/package.json +++ b/registry/agent/codex/package.json @@ -21,6 +21,7 @@ "@agentos-software/codex-cli": "workspace:*" }, "devDependencies": { + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.7.2" } diff --git a/registry/agent/codex/src/index.ts b/registry/agent/codex/src/index.ts index 531b82084..87b43b108 100644 --- a/registry/agent/codex/src/index.ts +++ b/registry/agent/codex/src/index.ts @@ -1,3 +1,8 @@ import codexSoftware from "@agentos-software/codex-cli"; +import type { PackageDescriptor } from "@agentos-software/manifest"; -export default codexSoftware; +// codex has no ACP adapter wired yet, so it re-exports the codex-cli software +// descriptor verbatim (plain command package, no `agent` block). +const codex: PackageDescriptor = codexSoftware; + +export default codex; diff --git a/registry/agent/codex/tests/package.test.mjs b/registry/agent/codex/tests/package.test.mjs index 79b4f1860..ae4fd884b 100644 --- a/registry/agent/codex/tests/package.test.mjs +++ b/registry/agent/codex/tests/package.test.mjs @@ -14,6 +14,6 @@ test("codex package does not advertise an ACP adapter until the real agent is wi assert.equal(manifest.bin, undefined); assert.equal(codex.name, "codex"); - assert.equal(typeof codex.commandDir, "string"); + assert.equal(typeof codex.dir, "string"); assert.equal(codex.agent, undefined); }); diff --git a/registry/agent/opencode/package.json b/registry/agent/opencode/package.json index 7786d5a82..cd6738f68 100644 --- a/registry/agent/opencode/package.json +++ b/registry/agent/opencode/package.json @@ -15,11 +15,15 @@ "default": "./dist/index.js" } }, + "files": [ + "dist" + ], "scripts": { - "build": "node ./scripts/build-opencode-acp.mjs && tsc", + "build": "node ./scripts/build-opencode-acp.mjs && tsc && node ../../../packages/agentos-toolchain/dist/cli.js pack . --out dist/package --agent agentos-opencode-acp", "check-types": "tsc --noEmit" }, "devDependencies": { + "@agentos-software/manifest": "workspace:*", "bun": "1.3.11", "@types/node": "^22.10.2", "typescript": "^5.7.2" diff --git a/registry/agent/opencode/src/adapter.ts b/registry/agent/opencode/src/adapter.ts index 866acdded..0ac881439 100644 --- a/registry/agent/opencode/src/adapter.ts +++ b/registry/agent/opencode/src/adapter.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env node process.env.OPENCODE_DISABLE_CONFIG_DEP_INSTALL ??= "1"; process.env.OPENCODE_DISABLE_EMBEDDED_WEB_UI ??= "1"; diff --git a/registry/agent/opencode/src/index.ts b/registry/agent/opencode/src/index.ts index e305cbaea..2dc4723e6 100644 --- a/registry/agent/opencode/src/index.ts +++ b/registry/agent/opencode/src/index.ts @@ -1,25 +1,17 @@ import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import type { PackageDescriptor } from "@agentos-software/manifest"; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const packageDir = resolve(__dirname, ".."); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const opencode = { +export default { name: "opencode", - type: "agent" as const, - packageDir, - requires: ["@agentos-software/opencode"], + dir, agent: { - id: "opencode", - // OpenCode still speaks ACP natively, but Agent OS runs a source-built - // Node ACP bundle entirely inside the VM rather than a host binary wrapper. - acpAdapter: "@agentos-software/opencode", - agentPackage: "@agentos-software/opencode", - staticEnv: { + acpEntrypoint: "agentos-opencode-acp", + env: { OPENCODE_DISABLE_CONFIG_DEP_INSTALL: "1", OPENCODE_DISABLE_EMBEDDED_WEB_UI: "1", }, }, -}; - -export default opencode; +} satisfies PackageDescriptor; diff --git a/registry/agent/pi-cli/package.json b/registry/agent/pi-cli/package.json index 08e63081e..6463fb19e 100644 --- a/registry/agent/pi-cli/package.json +++ b/registry/agent/pi-cli/package.json @@ -5,14 +5,21 @@ "license": "Apache-2.0", "main": "./dist/index.js", "types": "./dist/index.d.ts", + "bin": { + "pi-acp": "./node_modules/pi-acp/dist/index.js", + "pi": "./node_modules/@mariozechner/pi-coding-agent/dist/cli.js" + }, "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, + "files": [ + "dist" + ], "scripts": { - "build": "tsc", + "build": "tsc && node ../../../packages/agentos-toolchain/dist/cli.js pack . --out dist/package --agent pi-acp --prune-native", "check-types": "tsc --noEmit" }, "dependencies": { @@ -20,6 +27,7 @@ "pi-acp": "^0.0.23" }, "devDependencies": { + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.7.2" } diff --git a/registry/agent/pi-cli/src/index.ts b/registry/agent/pi-cli/src/index.ts index 16a18a647..2f56f0217 100644 --- a/registry/agent/pi-cli/src/index.ts +++ b/registry/agent/pi-cli/src/index.ts @@ -1,25 +1,19 @@ import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import type { PackageDescriptor } from "@agentos-software/manifest"; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const packageDir = resolve(__dirname, ".."); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const piCli = { +export default { name: "pi-cli", - type: "agent" as const, - packageDir, - requires: ["pi-acp", "@mariozechner/pi-coding-agent"], + dir, agent: { - id: "pi-cli", - acpAdapter: "pi-acp", - agentPackage: "@mariozechner/pi-coding-agent", - env: (ctx: { resolveBin(packageName: string, binName?: string): string }) => ({ - PI_ACP_PI_COMMAND: ctx.resolveBin( - "@mariozechner/pi-coding-agent", - "pi", - ), - }), + acpEntrypoint: "pi-acp", + // The pi CLI is projected onto $PATH as /opt/agentos/bin/pi (from this + // package's package.json "bin"), so point pi-acp at it by name. Replaces + // the old dynamic env (ctx.resolveBin), which no longer exists. + env: { + PI_ACP_PI_COMMAND: "pi", + }, }, -}; - -export default piCli; +} satisfies PackageDescriptor; diff --git a/registry/agent/pi/package.json b/registry/agent/pi/package.json index 472125065..e5d98c080 100644 --- a/registry/agent/pi/package.json +++ b/registry/agent/pi/package.json @@ -15,8 +15,11 @@ "default": "./dist/index.js" } }, + "files": [ + "dist" + ], "scripts": { - "build": "tsc && node scripts/build-snapshot-bundle.mjs", + "build": "tsc && node scripts/build-snapshot-bundle.mjs && node ../../../packages/agentos-toolchain/dist/cli.js pack . --out dist/package --agent pi-sdk-acp --prune-native && node scripts/copy-snapshot-into-package.mjs", "build:snapshot": "node scripts/build-snapshot-bundle.mjs", "check-types": "tsc --noEmit", "test": "pnpm build && node --test --test-force-exit tests/*.test.mjs" @@ -27,6 +30,7 @@ "@mariozechner/pi-ai": "0.60.0" }, "devDependencies": { + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.7.2" } diff --git a/registry/agent/pi/scripts/copy-snapshot-into-package.mjs b/registry/agent/pi/scripts/copy-snapshot-into-package.mjs new file mode 100644 index 000000000..01b032a3b --- /dev/null +++ b/registry/agent/pi/scripts/copy-snapshot-into-package.mjs @@ -0,0 +1,42 @@ +/** + * Copies the Pi SDK V8 snapshot bundle into the assembled `dist/package/` closure. + * + * The descriptor's `dir` points at `dist/package/` (the clean runtime closure the + * sidecar projects into `/opt/agentos/pi/`). The agent-os host reads the + * snapshot bundle from `/dist/sdk-snapshot.js` + * (`resolveAgentSnapshotBundle()` in `@rivet-dev/agentos-core`), so the bundle — + * built by `build-snapshot-bundle.mjs` at `dist/sdk-snapshot.js` — must be mirrored + * to `dist/package/dist/sdk-snapshot.js` (plus its `.sha256`). The toolchain `pack` + * rebuilds `dist/package/` from scratch, so this runs AFTER pack. + */ +import { copyFileSync, existsSync, mkdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const pkgRoot = join(here, ".."); +const srcDir = join(pkgRoot, "dist"); +const destDir = join(pkgRoot, "dist", "package", "dist"); + +const files = ["sdk-snapshot.js", "sdk-snapshot.js.sha256"]; + +const missing = files.filter((f) => !existsSync(join(srcDir, f))); +if (missing.length > 0) { + throw new Error( + `copy-snapshot-into-package: missing snapshot artifact(s) ${missing.join(", ")} in ${srcDir}; ` + + `run build-snapshot-bundle.mjs first`, + ); +} +if (!existsSync(join(pkgRoot, "dist", "package", "package.json"))) { + throw new Error( + `copy-snapshot-into-package: dist/package not assembled; run the toolchain pack first`, + ); +} + +mkdirSync(destDir, { recursive: true }); +for (const f of files) { + copyFileSync(join(srcDir, f), join(destDir, f)); +} +console.log( + `copy-snapshot-into-package: mirrored ${files.join(", ")} → dist/package/dist/`, +); diff --git a/registry/agent/pi/src/index.ts b/registry/agent/pi/src/index.ts index fcc7299dc..b6c048754 100644 --- a/registry/agent/pi/src/index.ts +++ b/registry/agent/pi/src/index.ts @@ -1,28 +1,18 @@ +import type { PackageDescriptor } from "@agentos-software/manifest"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const packageDir = resolve(__dirname, ".."); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pi = { +export default { name: "pi", - type: "agent" as const, - packageDir, - requires: [ - "@agentclientprotocol/sdk", - "@agentos-software/pi", - "@mariozechner/pi-coding-agent", - ], + dir, agent: { - id: "pi", - acpAdapter: "@agentos-software/pi", - agentPackage: "@mariozechner/pi-coding-agent", + acpEntrypoint: "pi-sdk-acp", // Evaluate the bundled Pi SDK into the per-sidecar V8 snapshot // (dist/sdk-snapshot.js) so it loads once per sidecar and is reused // across sessions. Falls back to per-session dynamic import if the // snapshot can't be built. snapshot: true, }, -}; - -export default pi; +} satisfies PackageDescriptor; diff --git a/registry/file-system/google-drive/README.md b/registry/file-system/google-drive/README.md deleted file mode 100644 index cc90031f5..000000000 --- a/registry/file-system/google-drive/README.md +++ /dev/null @@ -1,41 +0,0 @@ -> **Preview** This package is in preview and may have breaking changes. - -# @secure-exec/google-drive - -Declarative Google Drive native mount helper for secure-exec VMs. This package keeps -the public helper surface on the TypeScript side while routing first-party -Google Drive-backed filesystems through the native `google_drive` sidecar -plugin. - -## Usage - -```ts -import { createGoogleDriveBackend } from "@secure-exec/google-drive"; - -export const googleDriveMount = { - path: "/data", - plugin: createGoogleDriveBackend({ - credentials: { - clientEmail: "...", - privateKey: "...", - }, - folderId: "your-google-drive-folder-id", - }), -}; -``` - -## Configuration - -| Option | Type | Required | Description | -|--------|------|----------|-------------| -| `credentials` | `{ clientEmail: string; privateKey: string }` | Yes | Google service account credentials | -| `folderId` | `string` | Yes | Google Drive folder ID where blocks are stored | -| `keyPrefix` | `string` | No | Optional prefix for the persisted manifest and block file names | -| `chunkSize` | `number` | No | Optional persisted block chunk size used by the native plugin | -| `inlineThreshold` | `number` | No | Optional maximum inline file size stored in the manifest before chunking | - -## Rate Limits - -Google Drive API has a rate limit of approximately 10 queries/sec/user. Heavy -I/O workloads may experience throttling. Consider larger `chunkSize` values for -write-heavy workloads so the native plugin emits fewer Drive API calls. diff --git a/registry/file-system/google-drive/package.json b/registry/file-system/google-drive/package.json deleted file mode 100644 index 5ff182bbb..000000000 --- a/registry/file-system/google-drive/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@secure-exec/google-drive", - "version": "0.3.0-rc.1", - "type": "module", - "license": "Apache-2.0", - "beta": true, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "scripts": { - "check-types": "tsc --noEmit", - "build": "tsc", - "test": "vitest run" - }, - "dependencies": { - "@secure-exec/core": "workspace:*" - }, - "devDependencies": { - "@types/node": "^22.10.2", - "typescript": "^5.7.2", - "vitest": "^2.1.8" - } -} diff --git a/registry/file-system/google-drive/src/index.ts b/registry/file-system/google-drive/src/index.ts deleted file mode 100644 index e6621eda1..000000000 --- a/registry/file-system/google-drive/src/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { - MountConfigJsonObject, - NativeMountPluginDescriptor, -} from "@secure-exec/core/descriptors"; - -export type GoogleDriveCredentials = MountConfigJsonObject & { - clientEmail: string; - privateKey: string; -}; - -export interface GoogleDriveFsOptions { - credentials: GoogleDriveCredentials; - folderId: string; - keyPrefix?: string; - chunkSize?: number; - inlineThreshold?: number; -} - -export type GoogleDriveMountPluginConfig = MountConfigJsonObject & { - credentials: GoogleDriveCredentials; - folderId: string; - keyPrefix?: string; - chunkSize?: number; - inlineThreshold?: number; -}; - -/** - * Create a declarative Google Drive native mount descriptor. - * - * This keeps the package on the public mount-helper surface while routing - * first-party Google Drive-backed filesystems through the native - * `google_drive` plugin instead of a TypeScript runtime package. - */ -export function createGoogleDriveBackend( - options: GoogleDriveFsOptions, -): NativeMountPluginDescriptor { - return { - id: "google_drive", - config: { - credentials: options.credentials, - folderId: options.folderId, - ...(options.keyPrefix ? { keyPrefix: options.keyPrefix } : {}), - ...(options.chunkSize != null ? { chunkSize: options.chunkSize } : {}), - ...(options.inlineThreshold != null - ? { inlineThreshold: options.inlineThreshold } - : {}), - }, - }; -} diff --git a/registry/file-system/google-drive/tests/google-drive.test.ts b/registry/file-system/google-drive/tests/google-drive.test.ts deleted file mode 100644 index 6519c49a0..000000000 --- a/registry/file-system/google-drive/tests/google-drive.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { createGoogleDriveBackend } from "../src/index.js"; - -describe("@secure-exec/google-drive", () => { - it("serializes a native google_drive mount descriptor", () => { - expect( - createGoogleDriveBackend({ - credentials: { - clientEmail: "service-account@example.com", - privateKey: "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", - }, - folderId: "folder-123", - keyPrefix: "agentos/test", - chunkSize: 16, - inlineThreshold: 8, - }), - ).toEqual({ - id: "google_drive", - config: { - credentials: { - clientEmail: "service-account@example.com", - privateKey: "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", - }, - folderId: "folder-123", - keyPrefix: "agentos/test", - chunkSize: 16, - inlineThreshold: 8, - }, - }); - }); -}); diff --git a/registry/file-system/google-drive/tsconfig.json b/registry/file-system/google-drive/tsconfig.json deleted file mode 100644 index bff731325..000000000 --- a/registry/file-system/google-drive/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "declaration": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/registry/file-system/google-drive/vitest.config.ts b/registry/file-system/google-drive/vitest.config.ts deleted file mode 100644 index 4ba6db64a..000000000 --- a/registry/file-system/google-drive/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - testTimeout: 60000, - include: ["tests/**/*.test.ts"], - }, -}); diff --git a/registry/file-system/s3/package.json b/registry/file-system/s3/package.json deleted file mode 100644 index 49356e2d3..000000000 --- a/registry/file-system/s3/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@secure-exec/s3", - "version": "0.3.0-rc.1", - "type": "module", - "license": "Apache-2.0", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "scripts": { - "check-types": "tsc --noEmit", - "build": "tsc", - "test": "vitest run" - }, - "dependencies": { - "@secure-exec/core": "workspace:*" - }, - "devDependencies": { - "@types/node": "^22.10.2", - "typescript": "^5.7.2", - "vitest": "^2.1.8" - } -} diff --git a/registry/file-system/s3/src/index.ts b/registry/file-system/s3/src/index.ts deleted file mode 100644 index 3d32b2e5e..000000000 --- a/registry/file-system/s3/src/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { - MountConfigJsonObject, - NativeMountPluginDescriptor, -} from "@secure-exec/core/descriptors"; - -export type S3Credentials = MountConfigJsonObject & { - accessKeyId: string; - secretAccessKey: string; -}; - -export interface S3FsOptions { - bucket: string; - metadataPath: string; - prefix?: string; - region?: string; - credentials?: S3Credentials; - endpoint?: string; - chunkSize?: number; - inlineThreshold?: number; -} - -export type S3MountPluginConfig = MountConfigJsonObject & { - bucket: string; - metadataPath: string; - prefix?: string; - region?: string; - credentials?: S3Credentials; - endpoint?: string; - chunkSize?: number; - inlineThreshold?: number; -}; - -/** - * Create a declarative S3 mount plugin descriptor. - * - * This keeps the legacy helper name while routing first-party S3-backed mounts - * through the native `chunked_s3` plugin instead of a TypeScript runtime package. - */ -export function createS3Backend( - options: S3FsOptions, -): NativeMountPluginDescriptor { - return { - id: "chunked_s3", - config: { - bucket: options.bucket, - metadataPath: options.metadataPath, - ...(options.prefix ? { prefix: options.prefix } : {}), - ...(options.region ? { region: options.region } : {}), - ...(options.credentials ? { credentials: options.credentials } : {}), - ...(options.endpoint ? { endpoint: options.endpoint } : {}), - ...(options.chunkSize != null ? { chunkSize: options.chunkSize } : {}), - ...(options.inlineThreshold != null - ? { inlineThreshold: options.inlineThreshold } - : {}), - }, - }; -} diff --git a/registry/file-system/s3/tests/s3.test.ts b/registry/file-system/s3/tests/s3.test.ts deleted file mode 100644 index 0a659abac..000000000 --- a/registry/file-system/s3/tests/s3.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { createS3Backend } from "../src/index.js"; - -describe("@secure-exec/s3", () => { - it("serializes a native s3 mount descriptor", () => { - expect( - createS3Backend({ - bucket: "bucket-123", - metadataPath: "/tmp/secure-exec-s3.sqlite", - prefix: "descriptor-test", - region: "us-east-1", - endpoint: "https://s3.example.com", - credentials: { - accessKeyId: "access-key-id", - secretAccessKey: "secret-access-key", - }, - chunkSize: 16, - inlineThreshold: 8, - }), - ).toEqual({ - id: "chunked_s3", - config: { - bucket: "bucket-123", - metadataPath: "/tmp/secure-exec-s3.sqlite", - prefix: "descriptor-test", - region: "us-east-1", - endpoint: "https://s3.example.com", - credentials: { - accessKeyId: "access-key-id", - secretAccessKey: "secret-access-key", - }, - chunkSize: 16, - inlineThreshold: 8, - }, - }); - }); -}); diff --git a/registry/file-system/s3/tsconfig.json b/registry/file-system/s3/tsconfig.json deleted file mode 100644 index bff731325..000000000 --- a/registry/file-system/s3/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "declaration": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/registry/file-system/s3/vitest.config.ts b/registry/file-system/s3/vitest.config.ts deleted file mode 100644 index 4ba6db64a..000000000 --- a/registry/file-system/s3/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - testTimeout: 60000, - include: ["tests/**/*.test.ts"], - }, -}); diff --git a/registry/scripts/assemble-dist-package.mjs b/registry/scripts/assemble-dist-package.mjs new file mode 100644 index 000000000..57457fb73 --- /dev/null +++ b/registry/scripts/assemble-dist-package.mjs @@ -0,0 +1,139 @@ +#!/usr/bin/env node + +/** + * Assemble a clean `dist/package/` for a WASM / command registry package. + * + * Why this exists: a package's `src/index.ts` sets the descriptor `dir` that the + * sidecar projects read-only under `/opt/agentos//`. The sidecar + * copies that WHOLE directory verbatim. If `dir` points at the package source root + * it drags in `src/`, dev `node_modules/`, `tsconfig.json`, `.turbo/`, `dist/` — all + * mounted into every VM. The descriptor must instead point at a CLEAN runtime dir + * that holds ONLY what the package ships at runtime: + * + * dist/package/ + * package.json { name, version, bin: { : "bin/" } } + * bin/ the compiled WASM command binaries (copied verbatim) + * share/... (optional) man pages, if the source package ships them + * + * Commands are derived from the source `bin/` directory's files (the projection's + * WASM fallback links every file in `bin/`), and `name`/`version` are read from the + * source `package.json`. The src descriptor is then pointed at this dir, i.e. + * `dir = resolve(dirname(fileURLToPath(import.meta.url)), "package")` (from + * `dist/index.js` that resolves to `dist/package`). + * + * This handles the COMMON case (WASM / compiled command packages). It does NOT + * handle: + * - JS agent packages (pi, claude, opencode, pi-cli) whose `dist/package` is a + * self-contained node runtime closure — those are built with + * `@rivet-dev/agentos-toolchain pack ./ --out dist/package --agent `. + * - Meta packages (build-essential, common, everything) that default-export an + * ARRAY of descriptors and have no `dir`/`bin` of their own — they ship nothing. + * + * Usage (run from a package directory, or pass the package dir): + * node /abs/registry/scripts/assemble-dist-package.mjs [packageDir] + * + * Idempotent: the output `dist/package/` is removed and rebuilt on every run. + */ + +import { + cpSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import { basename, join, resolve } from "node:path"; + +function fail(message) { + process.stderr.write(`error: ${message}\n`); + process.exit(1); +} + +function main() { + const packageDir = resolve(process.argv[2] ?? process.cwd()); + + // 1. Read name + version from the source package.json. + const srcPkgPath = join(packageDir, "package.json"); + if (!existsSync(srcPkgPath)) { + fail(`no package.json in ${packageDir}`); + } + let srcPkg; + try { + srcPkg = JSON.parse(readFileSync(srcPkgPath, "utf8")); + } catch (error) { + fail(`package.json in ${packageDir} is not valid JSON: ${String(error)}`); + } + const name = srcPkg.name; + const version = srcPkg.version; + if (typeof name !== "string" || name.length === 0) { + fail(`package.json in ${packageDir} is missing a valid "name"`); + } + if (typeof version !== "string" || version.length === 0) { + fail(`package.json in ${packageDir} is missing a valid "version"`); + } + + // 2. Commands = the files in the source bin/ directory. This is the WASM / + // command case. A package with NO bin/ (or an empty one) is a PLANNED + // package whose binaries are built on upload and gitignored (e.g. make, + // zip, duckdb) — it ships a valid, empty placeholder now and picks up its + // commands automatically once the upload build populates bin/. (Meta + // packages that export an ARRAY never run this helper — they have no `dir`.) + const srcBinDir = join(packageDir, "bin"); + const hasBinDir = existsSync(srcBinDir) && statSync(srcBinDir).isDirectory(); + const commands = hasBinDir + ? readdirSync(srcBinDir, { withFileTypes: true }) + .filter((entry) => entry.isFile() || entry.isSymbolicLink()) + .map((entry) => entry.name) + .sort() + : []; + + // 3. (Re)create a clean output dir — idempotent. + const outDir = join(packageDir, "dist", "package"); + rmSync(outDir, { recursive: true, force: true }); + mkdirSync(outDir, { recursive: true }); + + // 4. Copy bin/ verbatim (preserve symlinks so any in-package relative aliases + // stay in-tree, matching the sidecar's verbatim projection copy). Skipped + // for a planned/empty package — there is nothing to copy yet. + if (commands.length > 0) { + cpSync(srcBinDir, join(outDir, "bin"), { + recursive: true, + verbatimSymlinks: true, + }); + } + + // 5. Copy share/ (man pages, etc.) if the package ships it — the projection + // builds a man symlink farm from /share/man. + const srcShareDir = join(packageDir, "share"); + if (existsSync(srcShareDir) && statSync(srcShareDir).isDirectory()) { + cpSync(srcShareDir, join(outDir, "share"), { + recursive: true, + verbatimSymlinks: true, + }); + } + + // 6. Write the clean runtime package.json. The bin map names every command + // explicitly (cmd -> bin/cmd); the sidecar reads `version` here and links + // each command into /opt/agentos/bin. + const bin = {}; + for (const cmd of commands) { + bin[cmd] = `bin/${basename(cmd)}`; + } + writeFileSync( + join(outDir, "package.json"), + `${JSON.stringify({ name, version, bin }, null, 2)}\n`, + ); + + process.stdout.write( + commands.length > 0 + ? `assembled ${name}@${version} -> ${outDir}\n` + + ` commands (${commands.length}): ${commands.join(", ")}\n` + : `assembled ${name}@${version} -> ${outDir} (PLANNED: empty placeholder, ` + + `no bin/ yet — commands appear once built on upload)\n`, + ); +} + +main(); diff --git a/registry/software/build-essential/package.json b/registry/software/build-essential/package.json index d85ba86b8..97c1b31ce 100644 --- a/registry/software/build-essential/package.json +++ b/registry/software/build-essential/package.json @@ -26,7 +26,7 @@ "@agentos-software/git": "workspace:*" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/build-essential/secure-exec-package.json b/registry/software/build-essential/secure-exec-package.json deleted file mode 100644 index 7251fe4a8..000000000 --- a/registry/software/build-essential/secure-exec-package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@agentos-software/build-essential", - "type": "meta", - "description": "Build-essential WASM command set (standard + make + git + curl)", - "includes": [ - "standard", - "make", - "git", - "curl" - ] -} diff --git a/registry/software/codex-cli/bin/codex b/registry/software/codex-cli/bin/codex new file mode 100755 index 000000000..7a9675404 Binary files /dev/null and b/registry/software/codex-cli/bin/codex differ diff --git a/registry/software/codex-cli/bin/codex-exec b/registry/software/codex-cli/bin/codex-exec new file mode 100755 index 000000000..be8fd24a8 Binary files /dev/null and b/registry/software/codex-cli/bin/codex-exec differ diff --git a/registry/software/codex/package.json b/registry/software/codex-cli/package.json similarity index 80% rename from registry/software/codex/package.json rename to registry/software/codex-cli/package.json index 4cfd68639..aba6aed83 100644 --- a/registry/software/codex/package.json +++ b/registry/software/codex-cli/package.json @@ -8,7 +8,7 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "bin" ], "exports": { ".": { @@ -17,11 +17,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/codex-cli/src/index.ts b/registry/software/codex-cli/src/index.ts new file mode 100644 index 000000000..055fc9d90 --- /dev/null +++ b/registry/software/codex-cli/src/index.ts @@ -0,0 +1,7 @@ +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); + +export default { name: "codex", dir } satisfies PackageDescriptor; diff --git a/registry/software/codex/tsconfig.json b/registry/software/codex-cli/tsconfig.json similarity index 100% rename from registry/software/codex/tsconfig.json rename to registry/software/codex-cli/tsconfig.json diff --git a/registry/software/codex/secure-exec-package.json b/registry/software/codex/secure-exec-package.json deleted file mode 100644 index 2879a29d9..000000000 --- a/registry/software/codex/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/codex-cli", - "type": "wasm", - "description": "OpenAI Codex command package (codex, codex-exec)", - "aptName": "codex", - "source": "rust" -} diff --git a/registry/software/codex/secure-exec-package.meta.json b/registry/software/codex/secure-exec-package.meta.json deleted file mode 100644 index 1b672da5c..000000000 --- a/registry/software/codex/secure-exec-package.meta.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "artifacts": { - "codex": { - "size": 29924563, - "sizeGzip": 9741514 - }, - "codex-exec": { - "size": 29924563, - "sizeGzip": 9741514 - } - }, - "totalSize": 59849126, - "totalSizeGzip": 19483028 -} diff --git a/registry/software/codex/src/index.ts b/registry/software/codex/src/index.ts deleted file mode 100644 index ef6594aff..000000000 --- a/registry/software/codex/src/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const pkg = { - name: "codex", - aptName: "codex", - description: "OpenAI Codex command package (codex, codex-exec)", - source: "rust" as const, - commands: [ - { name: "codex", permissionTier: "full" as const }, - { name: "codex-exec", permissionTier: "full" as const }, - ], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; diff --git a/registry/software/common/package.json b/registry/software/common/package.json index bd61edcfb..2c3a276fd 100644 --- a/registry/software/common/package.json +++ b/registry/software/common/package.json @@ -30,7 +30,7 @@ "@agentos-software/gzip": "workspace:*" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/common/secure-exec-package.json b/registry/software/common/secure-exec-package.json deleted file mode 100644 index 7a5e033aa..000000000 --- a/registry/software/common/secure-exec-package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@agentos-software/common", - "type": "meta", - "description": "Common WASM command set (coreutils + sed + grep + gawk + findutils + diffutils + tar + gzip)", - "includes": [ - "coreutils", - "sed", - "grep", - "gawk", - "findutils", - "diffutils", - "tar", - "gzip" - ] -} diff --git a/registry/software/coreutils/bin/[ b/registry/software/coreutils/bin/[ new file mode 100644 index 000000000..f47eaab09 Binary files /dev/null and b/registry/software/coreutils/bin/[ differ diff --git a/registry/software/coreutils/bin/_stubs b/registry/software/coreutils/bin/_stubs new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/_stubs differ diff --git a/registry/software/coreutils/bin/arch b/registry/software/coreutils/bin/arch new file mode 100644 index 000000000..49b881a53 Binary files /dev/null and b/registry/software/coreutils/bin/arch differ diff --git a/registry/software/coreutils/bin/b2sum b/registry/software/coreutils/bin/b2sum new file mode 100644 index 000000000..097af24f8 Binary files /dev/null and b/registry/software/coreutils/bin/b2sum differ diff --git a/registry/software/coreutils/bin/base32 b/registry/software/coreutils/bin/base32 new file mode 100644 index 000000000..41d7806d9 Binary files /dev/null and b/registry/software/coreutils/bin/base32 differ diff --git a/registry/software/coreutils/bin/base64 b/registry/software/coreutils/bin/base64 new file mode 100644 index 000000000..b531e115e Binary files /dev/null and b/registry/software/coreutils/bin/base64 differ diff --git a/registry/software/coreutils/bin/basename b/registry/software/coreutils/bin/basename new file mode 100644 index 000000000..74e382652 Binary files /dev/null and b/registry/software/coreutils/bin/basename differ diff --git a/registry/software/coreutils/bin/basenc b/registry/software/coreutils/bin/basenc new file mode 100644 index 000000000..d65638612 Binary files /dev/null and b/registry/software/coreutils/bin/basenc differ diff --git a/registry/software/coreutils/bin/bash b/registry/software/coreutils/bin/bash new file mode 100644 index 000000000..ba2074e22 Binary files /dev/null and b/registry/software/coreutils/bin/bash differ diff --git a/registry/software/coreutils/bin/cat b/registry/software/coreutils/bin/cat new file mode 100644 index 000000000..3c2b4538e Binary files /dev/null and b/registry/software/coreutils/bin/cat differ diff --git a/registry/software/coreutils/bin/chcon b/registry/software/coreutils/bin/chcon new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/chcon differ diff --git a/registry/software/coreutils/bin/chgrp b/registry/software/coreutils/bin/chgrp new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/chgrp differ diff --git a/registry/software/coreutils/bin/chmod b/registry/software/coreutils/bin/chmod new file mode 100644 index 000000000..fe3bbe8ff Binary files /dev/null and b/registry/software/coreutils/bin/chmod differ diff --git a/registry/software/coreutils/bin/chown b/registry/software/coreutils/bin/chown new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/chown differ diff --git a/registry/software/coreutils/bin/chroot b/registry/software/coreutils/bin/chroot new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/chroot differ diff --git a/registry/software/coreutils/bin/cksum b/registry/software/coreutils/bin/cksum new file mode 100644 index 000000000..5ca945e2a Binary files /dev/null and b/registry/software/coreutils/bin/cksum differ diff --git a/registry/software/coreutils/bin/column b/registry/software/coreutils/bin/column new file mode 100644 index 000000000..132a83537 Binary files /dev/null and b/registry/software/coreutils/bin/column differ diff --git a/registry/software/coreutils/bin/comm b/registry/software/coreutils/bin/comm new file mode 100644 index 000000000..765f03293 Binary files /dev/null and b/registry/software/coreutils/bin/comm differ diff --git a/registry/software/coreutils/bin/cp b/registry/software/coreutils/bin/cp new file mode 100644 index 000000000..37343d199 Binary files /dev/null and b/registry/software/coreutils/bin/cp differ diff --git a/registry/software/coreutils/bin/cut b/registry/software/coreutils/bin/cut new file mode 100644 index 000000000..8dc692921 Binary files /dev/null and b/registry/software/coreutils/bin/cut differ diff --git a/registry/software/coreutils/bin/date b/registry/software/coreutils/bin/date new file mode 100644 index 000000000..4f9ca769a Binary files /dev/null and b/registry/software/coreutils/bin/date differ diff --git a/registry/software/coreutils/bin/dd b/registry/software/coreutils/bin/dd new file mode 100644 index 000000000..34d77bb39 Binary files /dev/null and b/registry/software/coreutils/bin/dd differ diff --git a/registry/software/coreutils/bin/df b/registry/software/coreutils/bin/df new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/df differ diff --git a/registry/software/coreutils/bin/dir b/registry/software/coreutils/bin/dir new file mode 100644 index 000000000..fc6645747 Binary files /dev/null and b/registry/software/coreutils/bin/dir differ diff --git a/registry/software/coreutils/bin/dircolors b/registry/software/coreutils/bin/dircolors new file mode 100644 index 000000000..705c61064 Binary files /dev/null and b/registry/software/coreutils/bin/dircolors differ diff --git a/registry/software/coreutils/bin/dirname b/registry/software/coreutils/bin/dirname new file mode 100644 index 000000000..aa4227932 Binary files /dev/null and b/registry/software/coreutils/bin/dirname differ diff --git a/registry/software/coreutils/bin/du b/registry/software/coreutils/bin/du new file mode 100644 index 000000000..8bf2b20d3 Binary files /dev/null and b/registry/software/coreutils/bin/du differ diff --git a/registry/software/coreutils/bin/echo b/registry/software/coreutils/bin/echo new file mode 100644 index 000000000..26dac3b61 Binary files /dev/null and b/registry/software/coreutils/bin/echo differ diff --git a/registry/software/coreutils/bin/env b/registry/software/coreutils/bin/env new file mode 100644 index 000000000..2bf942b5d Binary files /dev/null and b/registry/software/coreutils/bin/env differ diff --git a/registry/software/coreutils/bin/expand b/registry/software/coreutils/bin/expand new file mode 100644 index 000000000..a6ec9d398 Binary files /dev/null and b/registry/software/coreutils/bin/expand differ diff --git a/registry/software/coreutils/bin/expr b/registry/software/coreutils/bin/expr new file mode 100644 index 000000000..a165b3e50 Binary files /dev/null and b/registry/software/coreutils/bin/expr differ diff --git a/registry/software/coreutils/bin/factor b/registry/software/coreutils/bin/factor new file mode 100644 index 000000000..530dae408 Binary files /dev/null and b/registry/software/coreutils/bin/factor differ diff --git a/registry/software/coreutils/bin/false b/registry/software/coreutils/bin/false new file mode 100644 index 000000000..3a00ccd2c Binary files /dev/null and b/registry/software/coreutils/bin/false differ diff --git a/registry/software/coreutils/bin/fmt b/registry/software/coreutils/bin/fmt new file mode 100644 index 000000000..04cdaf0be Binary files /dev/null and b/registry/software/coreutils/bin/fmt differ diff --git a/registry/software/coreutils/bin/fold b/registry/software/coreutils/bin/fold new file mode 100644 index 000000000..2b30a0f45 Binary files /dev/null and b/registry/software/coreutils/bin/fold differ diff --git a/registry/software/coreutils/bin/groups b/registry/software/coreutils/bin/groups new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/groups differ diff --git a/registry/software/coreutils/bin/head b/registry/software/coreutils/bin/head new file mode 100644 index 000000000..661ed1dd3 Binary files /dev/null and b/registry/software/coreutils/bin/head differ diff --git a/registry/software/coreutils/bin/hostid b/registry/software/coreutils/bin/hostid new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/hostid differ diff --git a/registry/software/coreutils/bin/hostname b/registry/software/coreutils/bin/hostname new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/hostname differ diff --git a/registry/software/coreutils/bin/id b/registry/software/coreutils/bin/id new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/id differ diff --git a/registry/software/coreutils/bin/install b/registry/software/coreutils/bin/install new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/install differ diff --git a/registry/software/coreutils/bin/join b/registry/software/coreutils/bin/join new file mode 100644 index 000000000..e9db3c686 Binary files /dev/null and b/registry/software/coreutils/bin/join differ diff --git a/registry/software/coreutils/bin/kill b/registry/software/coreutils/bin/kill new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/kill differ diff --git a/registry/software/coreutils/bin/link b/registry/software/coreutils/bin/link new file mode 100644 index 000000000..add19e982 Binary files /dev/null and b/registry/software/coreutils/bin/link differ diff --git a/registry/software/coreutils/bin/ln b/registry/software/coreutils/bin/ln new file mode 100644 index 000000000..797c31b30 Binary files /dev/null and b/registry/software/coreutils/bin/ln differ diff --git a/registry/software/coreutils/bin/logname b/registry/software/coreutils/bin/logname new file mode 100644 index 000000000..665f87f48 Binary files /dev/null and b/registry/software/coreutils/bin/logname differ diff --git a/registry/software/coreutils/bin/ls b/registry/software/coreutils/bin/ls new file mode 100644 index 000000000..fc6645747 Binary files /dev/null and b/registry/software/coreutils/bin/ls differ diff --git a/registry/software/coreutils/bin/md5sum b/registry/software/coreutils/bin/md5sum new file mode 100644 index 000000000..86b014590 Binary files /dev/null and b/registry/software/coreutils/bin/md5sum differ diff --git a/registry/software/coreutils/bin/mkdir b/registry/software/coreutils/bin/mkdir new file mode 100644 index 000000000..9d64fecba Binary files /dev/null and b/registry/software/coreutils/bin/mkdir differ diff --git a/registry/software/coreutils/bin/mkfifo b/registry/software/coreutils/bin/mkfifo new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/mkfifo differ diff --git a/registry/software/coreutils/bin/mknod b/registry/software/coreutils/bin/mknod new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/mknod differ diff --git a/registry/software/coreutils/bin/mktemp b/registry/software/coreutils/bin/mktemp new file mode 100644 index 000000000..bc6116053 Binary files /dev/null and b/registry/software/coreutils/bin/mktemp differ diff --git a/registry/software/coreutils/bin/more b/registry/software/coreutils/bin/more new file mode 100644 index 000000000..3c2b4538e Binary files /dev/null and b/registry/software/coreutils/bin/more differ diff --git a/registry/software/coreutils/bin/mv b/registry/software/coreutils/bin/mv new file mode 100644 index 000000000..10ba02048 Binary files /dev/null and b/registry/software/coreutils/bin/mv differ diff --git a/registry/software/coreutils/bin/nice b/registry/software/coreutils/bin/nice new file mode 100644 index 000000000..c2b3a18b9 Binary files /dev/null and b/registry/software/coreutils/bin/nice differ diff --git a/registry/software/coreutils/bin/nl b/registry/software/coreutils/bin/nl new file mode 100644 index 000000000..a2a11ed6b Binary files /dev/null and b/registry/software/coreutils/bin/nl differ diff --git a/registry/software/coreutils/bin/nohup b/registry/software/coreutils/bin/nohup new file mode 100644 index 000000000..ce94bd4b0 Binary files /dev/null and b/registry/software/coreutils/bin/nohup differ diff --git a/registry/software/coreutils/bin/nproc b/registry/software/coreutils/bin/nproc new file mode 100644 index 000000000..b9922e81d Binary files /dev/null and b/registry/software/coreutils/bin/nproc differ diff --git a/registry/software/coreutils/bin/numfmt b/registry/software/coreutils/bin/numfmt new file mode 100644 index 000000000..ab304adce Binary files /dev/null and b/registry/software/coreutils/bin/numfmt differ diff --git a/registry/software/coreutils/bin/od b/registry/software/coreutils/bin/od new file mode 100644 index 000000000..d1bda40a7 Binary files /dev/null and b/registry/software/coreutils/bin/od differ diff --git a/registry/software/coreutils/bin/paste b/registry/software/coreutils/bin/paste new file mode 100644 index 000000000..0f141c570 Binary files /dev/null and b/registry/software/coreutils/bin/paste differ diff --git a/registry/software/coreutils/bin/pathchk b/registry/software/coreutils/bin/pathchk new file mode 100644 index 000000000..e11f174cb Binary files /dev/null and b/registry/software/coreutils/bin/pathchk differ diff --git a/registry/software/coreutils/bin/pinky b/registry/software/coreutils/bin/pinky new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/pinky differ diff --git a/registry/software/coreutils/bin/printenv b/registry/software/coreutils/bin/printenv new file mode 100644 index 000000000..d49043747 Binary files /dev/null and b/registry/software/coreutils/bin/printenv differ diff --git a/registry/software/coreutils/bin/printf b/registry/software/coreutils/bin/printf new file mode 100644 index 000000000..885922e4f Binary files /dev/null and b/registry/software/coreutils/bin/printf differ diff --git a/registry/software/coreutils/bin/ptx b/registry/software/coreutils/bin/ptx new file mode 100644 index 000000000..3ee68cd87 Binary files /dev/null and b/registry/software/coreutils/bin/ptx differ diff --git a/registry/software/coreutils/bin/pwd b/registry/software/coreutils/bin/pwd new file mode 100644 index 000000000..df2bdd89e Binary files /dev/null and b/registry/software/coreutils/bin/pwd differ diff --git a/registry/software/coreutils/bin/readlink b/registry/software/coreutils/bin/readlink new file mode 100644 index 000000000..bceffe2f3 Binary files /dev/null and b/registry/software/coreutils/bin/readlink differ diff --git a/registry/software/coreutils/bin/realpath b/registry/software/coreutils/bin/realpath new file mode 100644 index 000000000..149b9c5aa Binary files /dev/null and b/registry/software/coreutils/bin/realpath differ diff --git a/registry/software/coreutils/bin/rev b/registry/software/coreutils/bin/rev new file mode 100644 index 000000000..3f90ee450 Binary files /dev/null and b/registry/software/coreutils/bin/rev differ diff --git a/registry/software/coreutils/bin/rm b/registry/software/coreutils/bin/rm new file mode 100644 index 000000000..ef8b5b8d7 Binary files /dev/null and b/registry/software/coreutils/bin/rm differ diff --git a/registry/software/coreutils/bin/rmdir b/registry/software/coreutils/bin/rmdir new file mode 100644 index 000000000..b10a8c696 Binary files /dev/null and b/registry/software/coreutils/bin/rmdir differ diff --git a/registry/software/coreutils/bin/runcon b/registry/software/coreutils/bin/runcon new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/runcon differ diff --git a/registry/software/coreutils/bin/seq b/registry/software/coreutils/bin/seq new file mode 100644 index 000000000..1d6594395 Binary files /dev/null and b/registry/software/coreutils/bin/seq differ diff --git a/registry/software/coreutils/bin/sh b/registry/software/coreutils/bin/sh new file mode 100644 index 000000000..ba2074e22 Binary files /dev/null and b/registry/software/coreutils/bin/sh differ diff --git a/registry/software/coreutils/bin/sha1sum b/registry/software/coreutils/bin/sha1sum new file mode 100644 index 000000000..6695e5390 Binary files /dev/null and b/registry/software/coreutils/bin/sha1sum differ diff --git a/registry/software/coreutils/bin/sha224sum b/registry/software/coreutils/bin/sha224sum new file mode 100644 index 000000000..7fbdd2e68 Binary files /dev/null and b/registry/software/coreutils/bin/sha224sum differ diff --git a/registry/software/coreutils/bin/sha256sum b/registry/software/coreutils/bin/sha256sum new file mode 100644 index 000000000..8d09cde66 Binary files /dev/null and b/registry/software/coreutils/bin/sha256sum differ diff --git a/registry/software/coreutils/bin/sha384sum b/registry/software/coreutils/bin/sha384sum new file mode 100644 index 000000000..a3f64b0c4 Binary files /dev/null and b/registry/software/coreutils/bin/sha384sum differ diff --git a/registry/software/coreutils/bin/sha512sum b/registry/software/coreutils/bin/sha512sum new file mode 100644 index 000000000..6b993e626 Binary files /dev/null and b/registry/software/coreutils/bin/sha512sum differ diff --git a/registry/software/coreutils/bin/shred b/registry/software/coreutils/bin/shred new file mode 100644 index 000000000..2916c4e88 Binary files /dev/null and b/registry/software/coreutils/bin/shred differ diff --git a/registry/software/coreutils/bin/shuf b/registry/software/coreutils/bin/shuf new file mode 100644 index 000000000..d16b25a3d Binary files /dev/null and b/registry/software/coreutils/bin/shuf differ diff --git a/registry/software/coreutils/bin/sleep b/registry/software/coreutils/bin/sleep new file mode 100644 index 000000000..407dcde60 Binary files /dev/null and b/registry/software/coreutils/bin/sleep differ diff --git a/registry/software/coreutils/bin/sort b/registry/software/coreutils/bin/sort new file mode 100644 index 000000000..ed33cfe71 Binary files /dev/null and b/registry/software/coreutils/bin/sort differ diff --git a/registry/software/coreutils/bin/split b/registry/software/coreutils/bin/split new file mode 100644 index 000000000..627ed10bd Binary files /dev/null and b/registry/software/coreutils/bin/split differ diff --git a/registry/software/coreutils/bin/stat b/registry/software/coreutils/bin/stat new file mode 100644 index 000000000..b1ef001ec Binary files /dev/null and b/registry/software/coreutils/bin/stat differ diff --git a/registry/software/coreutils/bin/stdbuf b/registry/software/coreutils/bin/stdbuf new file mode 100644 index 000000000..c2e68eb71 Binary files /dev/null and b/registry/software/coreutils/bin/stdbuf differ diff --git a/registry/software/coreutils/bin/strings b/registry/software/coreutils/bin/strings new file mode 100644 index 000000000..2409104dd Binary files /dev/null and b/registry/software/coreutils/bin/strings differ diff --git a/registry/software/coreutils/bin/stty b/registry/software/coreutils/bin/stty new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/stty differ diff --git a/registry/software/coreutils/bin/sum b/registry/software/coreutils/bin/sum new file mode 100644 index 000000000..4e5c41071 Binary files /dev/null and b/registry/software/coreutils/bin/sum differ diff --git a/registry/software/coreutils/bin/sync b/registry/software/coreutils/bin/sync new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/sync differ diff --git a/registry/software/coreutils/bin/tac b/registry/software/coreutils/bin/tac new file mode 100644 index 000000000..342f902c9 Binary files /dev/null and b/registry/software/coreutils/bin/tac differ diff --git a/registry/software/coreutils/bin/tail b/registry/software/coreutils/bin/tail new file mode 100644 index 000000000..ac2b8adb0 Binary files /dev/null and b/registry/software/coreutils/bin/tail differ diff --git a/registry/software/coreutils/bin/tee b/registry/software/coreutils/bin/tee new file mode 100644 index 000000000..d0224bb00 Binary files /dev/null and b/registry/software/coreutils/bin/tee differ diff --git a/registry/software/coreutils/bin/test b/registry/software/coreutils/bin/test new file mode 100644 index 000000000..f47eaab09 Binary files /dev/null and b/registry/software/coreutils/bin/test differ diff --git a/registry/software/coreutils/bin/timeout b/registry/software/coreutils/bin/timeout new file mode 100644 index 000000000..4e42ec380 Binary files /dev/null and b/registry/software/coreutils/bin/timeout differ diff --git a/registry/software/coreutils/bin/touch b/registry/software/coreutils/bin/touch new file mode 100644 index 000000000..74a41bbeb Binary files /dev/null and b/registry/software/coreutils/bin/touch differ diff --git a/registry/software/coreutils/bin/tr b/registry/software/coreutils/bin/tr new file mode 100644 index 000000000..91da09080 Binary files /dev/null and b/registry/software/coreutils/bin/tr differ diff --git a/registry/software/coreutils/bin/true b/registry/software/coreutils/bin/true new file mode 100644 index 000000000..0c33b6747 Binary files /dev/null and b/registry/software/coreutils/bin/true differ diff --git a/registry/software/coreutils/bin/truncate b/registry/software/coreutils/bin/truncate new file mode 100644 index 000000000..9ea61c269 Binary files /dev/null and b/registry/software/coreutils/bin/truncate differ diff --git a/registry/software/coreutils/bin/tsort b/registry/software/coreutils/bin/tsort new file mode 100644 index 000000000..011949031 Binary files /dev/null and b/registry/software/coreutils/bin/tsort differ diff --git a/registry/software/coreutils/bin/tty b/registry/software/coreutils/bin/tty new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/tty differ diff --git a/registry/software/coreutils/bin/uname b/registry/software/coreutils/bin/uname new file mode 100644 index 000000000..f673dbafc Binary files /dev/null and b/registry/software/coreutils/bin/uname differ diff --git a/registry/software/coreutils/bin/unexpand b/registry/software/coreutils/bin/unexpand new file mode 100644 index 000000000..855b9b6dd Binary files /dev/null and b/registry/software/coreutils/bin/unexpand differ diff --git a/registry/software/coreutils/bin/uniq b/registry/software/coreutils/bin/uniq new file mode 100644 index 000000000..5e4e67c77 Binary files /dev/null and b/registry/software/coreutils/bin/uniq differ diff --git a/registry/software/coreutils/bin/unlink b/registry/software/coreutils/bin/unlink new file mode 100644 index 000000000..dbe1ab4e1 Binary files /dev/null and b/registry/software/coreutils/bin/unlink differ diff --git a/registry/software/coreutils/bin/uptime b/registry/software/coreutils/bin/uptime new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/uptime differ diff --git a/registry/software/coreutils/bin/users b/registry/software/coreutils/bin/users new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/users differ diff --git a/registry/software/coreutils/bin/vdir b/registry/software/coreutils/bin/vdir new file mode 100644 index 000000000..fc6645747 Binary files /dev/null and b/registry/software/coreutils/bin/vdir differ diff --git a/registry/software/coreutils/bin/wc b/registry/software/coreutils/bin/wc new file mode 100644 index 000000000..4399154a8 Binary files /dev/null and b/registry/software/coreutils/bin/wc differ diff --git a/registry/software/coreutils/bin/which b/registry/software/coreutils/bin/which new file mode 100644 index 000000000..92a48da7b Binary files /dev/null and b/registry/software/coreutils/bin/which differ diff --git a/registry/software/coreutils/bin/who b/registry/software/coreutils/bin/who new file mode 100644 index 000000000..8988130b6 Binary files /dev/null and b/registry/software/coreutils/bin/who differ diff --git a/registry/software/coreutils/bin/whoami b/registry/software/coreutils/bin/whoami new file mode 100644 index 000000000..aa1bb62f0 Binary files /dev/null and b/registry/software/coreutils/bin/whoami differ diff --git a/registry/software/coreutils/bin/xu b/registry/software/coreutils/bin/xu new file mode 100644 index 000000000..b88238343 Binary files /dev/null and b/registry/software/coreutils/bin/xu differ diff --git a/registry/software/coreutils/bin/yes b/registry/software/coreutils/bin/yes new file mode 100644 index 000000000..d7e26df1e Binary files /dev/null and b/registry/software/coreutils/bin/yes differ diff --git a/registry/software/coreutils/package.json b/registry/software/coreutils/package.json index bff0ca5f0..d882a854b 100644 --- a/registry/software/coreutils/package.json +++ b/registry/software/coreutils/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/coreutils/secure-exec-package.json b/registry/software/coreutils/secure-exec-package.json deleted file mode 100644 index 472b44bc9..000000000 --- a/registry/software/coreutils/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/coreutils", - "type": "wasm", - "description": "GNU coreutils: sh, cat, ls, cp, sort, and 80+ commands", - "aptName": "coreutils", - "source": "rust" -} diff --git a/registry/software/coreutils/secure-exec-package.meta.json b/registry/software/coreutils/secure-exec-package.meta.json deleted file mode 100644 index 5309585d9..000000000 --- a/registry/software/coreutils/secure-exec-package.meta.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "artifacts": { - "[": { - "size": 36672, - "sizeGzip": 15980 - }, - "arch": { - "size": 345928, - "sizeGzip": 151828 - }, - "b2sum": { - "size": 1199617, - "sizeGzip": 824041 - }, - "base32": { - "size": 430821, - "sizeGzip": 173678 - }, - "base64": { - "size": 382180, - "sizeGzip": 164509 - }, - "basename": { - "size": 364368, - "sizeGzip": 158691 - }, - "basenc": { - "size": 450406, - "sizeGzip": 180031 - }, - "bash": { - "size": 2593996, - "sizeGzip": 943246 - }, - "cat": { - "size": 372104, - "sizeGzip": 159950 - }, - "chcon": { - "size": 48907, - "sizeGzip": 22371 - }, - "chgrp": { - "size": 48907, - "sizeGzip": 22371 - }, - "chmod": { - "size": 386485, - "sizeGzip": 167235 - }, - "chown": { - "size": 48907, - "sizeGzip": 22371 - }, - "chroot": { - "size": 48907, - "sizeGzip": 22372 - }, - "cksum": { - "size": 1217991, - "sizeGzip": 831420 - }, - "column": { - "size": 60271, - "sizeGzip": 27529 - }, - "comm": { - "size": 375469, - "sizeGzip": 162652 - }, - "cp": { - "size": 549396, - "sizeGzip": 235494 - }, - "cut": { - "size": 400213, - "sizeGzip": 170148 - }, - "date": { - "size": 2466523, - "sizeGzip": 877490 - }, - "dd": { - "size": 451732, - "sizeGzip": 196439 - }, - "df": { - "size": 48907, - "sizeGzip": 22368 - }, - "dir": { - "size": 1053349, - "sizeGzip": 404608 - }, - "dircolors": { - "size": 401087, - "sizeGzip": 175220 - }, - "dirname": { - "size": 346027, - "sizeGzip": 151943 - }, - "du": { - "size": 84827, - "sizeGzip": 39542 - }, - "echo": { - "size": 291818, - "sizeGzip": 129283 - }, - "env": { - "size": 63043, - "sizeGzip": 28575 - }, - "expand": { - "size": 380789, - "sizeGzip": 165810 - }, - "expr": { - "size": 894575, - "sizeGzip": 322044 - }, - "factor": { - "size": 561123, - "sizeGzip": 270189 - }, - "false": { - "size": 266519, - "sizeGzip": 119577 - }, - "fmt": { - "size": 395677, - "sizeGzip": 170766 - }, - "fold": { - "size": 380120, - "sizeGzip": 165263 - }, - "groups": { - "size": 48907, - "sizeGzip": 22372 - }, - "head": { - "size": 399085, - "sizeGzip": 170871 - }, - "hostid": { - "size": 48907, - "sizeGzip": 22372 - }, - "hostname": { - "size": 48907, - "sizeGzip": 22374 - }, - "id": { - "size": 48907, - "sizeGzip": 22368 - }, - "install": { - "size": 48907, - "sizeGzip": 22373 - }, - "join": { - "size": 1619076, - "sizeGzip": 605092 - }, - "kill": { - "size": 48907, - "sizeGzip": 22370 - }, - "link": { - "size": 359975, - "sizeGzip": 156923 - }, - "ln": { - "size": 397265, - "sizeGzip": 170877 - }, - "logname": { - "size": 345675, - "sizeGzip": 151717 - }, - "ls": { - "size": 1053349, - "sizeGzip": 404607 - }, - "md5sum": { - "size": 1197053, - "sizeGzip": 823110 - }, - "mkdir": { - "size": 368946, - "sizeGzip": 160484 - }, - "mkfifo": { - "size": 48907, - "sizeGzip": 22372 - }, - "mknod": { - "size": 48907, - "sizeGzip": 22371 - }, - "mktemp": { - "size": 381637, - "sizeGzip": 165247 - }, - "more": { - "size": 372104, - "sizeGzip": 159951 - }, - "mv": { - "size": 491694, - "sizeGzip": 212277 - }, - "nice": { - "size": 51122, - "sizeGzip": 23207 - }, - "nl": { - "size": 1211101, - "sizeGzip": 458254 - }, - "nohup": { - "size": 42420, - "sizeGzip": 18453 - }, - "nproc": { - "size": 357830, - "sizeGzip": 155752 - }, - "numfmt": { - "size": 431654, - "sizeGzip": 188247 - }, - "od": { - "size": 434786, - "sizeGzip": 182787 - }, - "paste": { - "size": 371795, - "sizeGzip": 162361 - }, - "pathchk": { - "size": 363016, - "sizeGzip": 157423 - }, - "pinky": { - "size": 48907, - "sizeGzip": 22371 - }, - "printenv": { - "size": 350425, - "sizeGzip": 153802 - }, - "printf": { - "size": 498872, - "sizeGzip": 222188 - }, - "ptx": { - "size": 1262244, - "sizeGzip": 480716 - }, - "pwd": { - "size": 349145, - "sizeGzip": 153358 - }, - "readlink": { - "size": 377667, - "sizeGzip": 163870 - }, - "realpath": { - "size": 382083, - "sizeGzip": 165248 - }, - "rev": { - "size": 53639, - "sizeGzip": 24577 - }, - "rm": { - "size": 450067, - "sizeGzip": 196262 - }, - "rmdir": { - "size": 363175, - "sizeGzip": 157887 - }, - "runcon": { - "size": 48907, - "sizeGzip": 22372 - }, - "seq": { - "size": 481478, - "sizeGzip": 214215 - }, - "sh": { - "size": 2593996, - "sizeGzip": 943244 - }, - "sha1sum": { - "size": 1197053, - "sizeGzip": 823107 - }, - "sha224sum": { - "size": 1197067, - "sizeGzip": 823114 - }, - "sha256sum": { - "size": 1197070, - "sizeGzip": 823117 - }, - "sha384sum": { - "size": 1197070, - "sizeGzip": 823120 - }, - "sha512sum": { - "size": 1197070, - "sizeGzip": 823116 - }, - "shred": { - "size": 395885, - "sizeGzip": 172116 - }, - "shuf": { - "size": 400378, - "sizeGzip": 173870 - }, - "sleep": { - "size": 57816, - "sizeGzip": 33173 - }, - "sort": { - "size": 1835218, - "sizeGzip": 700922 - }, - "split": { - "size": 425087, - "sizeGzip": 183609 - }, - "stat": { - "size": 812836, - "sizeGzip": 312993 - }, - "stdbuf": { - "size": 42916, - "sizeGzip": 18697 - }, - "strings": { - "size": 56007, - "sizeGzip": 25454 - }, - "stty": { - "size": 48907, - "sizeGzip": 22370 - }, - "_stubs": { - "size": 48907, - "sizeGzip": 22372 - }, - "sum": { - "size": 370882, - "sizeGzip": 160710 - }, - "sync": { - "size": 48907, - "sizeGzip": 22370 - }, - "tac": { - "size": 1206724, - "sizeGzip": 457521 - }, - "tail": { - "size": 547478, - "sizeGzip": 237471 - }, - "tee": { - "size": 373691, - "sizeGzip": 162668 - }, - "test": { - "size": 36672, - "sizeGzip": 15983 - }, - "timeout": { - "size": 73132, - "sizeGzip": 40887 - }, - "touch": { - "size": 874993, - "sizeGzip": 336639 - }, - "tr": { - "size": 400468, - "sizeGzip": 172893 - }, - "true": { - "size": 266539, - "sizeGzip": 119581 - }, - "truncate": { - "size": 371530, - "sizeGzip": 161815 - }, - "tsort": { - "size": 383763, - "sizeGzip": 165572 - }, - "tty": { - "size": 48907, - "sizeGzip": 22369 - }, - "uname": { - "size": 349608, - "sizeGzip": 152705 - }, - "unexpand": { - "size": 381307, - "sizeGzip": 165689 - }, - "uniq": { - "size": 396477, - "sizeGzip": 169966 - }, - "unlink": { - "size": 359040, - "sizeGzip": 156321 - }, - "uptime": { - "size": 48907, - "sizeGzip": 22372 - }, - "users": { - "size": 48907, - "sizeGzip": 22371 - }, - "vdir": { - "size": 1053349, - "sizeGzip": 404609 - }, - "wc": { - "size": 450560, - "sizeGzip": 192278 - }, - "which": { - "size": 46463, - "sizeGzip": 20294 - }, - "who": { - "size": 48907, - "sizeGzip": 22369 - }, - "whoami": { - "size": 38996, - "sizeGzip": 16956 - }, - "yes": { - "size": 344804, - "sizeGzip": 151460 - } - }, - "totalSize": 54029403, - "totalSizeGzip": 24676775 -} diff --git a/registry/software/coreutils/src/index.ts b/registry/software/coreutils/src/index.ts index b5690ab5a..57de6d8e8 100644 --- a/registry/software/coreutils/src/index.ts +++ b/registry/software/coreutils/src/index.ts @@ -1,186 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "coreutils", - aptName: "coreutils", - description: - "GNU coreutils: sh, 80+ commands, stubs, checksums, base encoding", - source: "rust" as const, - commands: [ - // Shell - { name: "sh", permissionTier: "full" as const }, - { name: "bash", permissionTier: "full" as const, aliasOf: "sh" }, - - // Process control shims (need spawn capability) - { name: "env", permissionTier: "full" as const }, - { name: "timeout", permissionTier: "full" as const }, - { name: "nice", permissionTier: "full" as const }, - { name: "nohup", permissionTier: "full" as const }, - { name: "stdbuf", permissionTier: "full" as const }, - - // File operations (read-write) - { name: "chmod", permissionTier: "read-write" as const }, - { name: "cp", permissionTier: "read-write" as const }, - // These binaries import `host_process` in the shipped Wasm artifacts. - { name: "dd", permissionTier: "full" as const }, - { name: "link", permissionTier: "read-write" as const }, - { name: "ln", permissionTier: "read-write" as const }, - { name: "mkdir", permissionTier: "read-write" as const }, - { name: "mktemp", permissionTier: "read-write" as const }, - { name: "mv", permissionTier: "read-write" as const }, - { name: "rm", permissionTier: "read-write" as const }, - { name: "rmdir", permissionTier: "read-write" as const }, - { name: "shred", permissionTier: "full" as const }, - { name: "split", permissionTier: "read-write" as const }, - { name: "touch", permissionTier: "read-write" as const }, - { name: "truncate", permissionTier: "read-write" as const }, - { name: "unlink", permissionTier: "read-write" as const }, - - // File operations (read-only) - { name: "cat", permissionTier: "read-only" as const }, - { name: "more", permissionTier: "read-only" as const, aliasOf: "cat" }, - { name: "head", permissionTier: "read-only" as const }, - { name: "tail", permissionTier: "read-only" as const }, - { name: "ls", permissionTier: "read-only" as const }, - { name: "dir", permissionTier: "read-only" as const, aliasOf: "ls" }, - { name: "vdir", permissionTier: "read-only" as const, aliasOf: "ls" }, - { name: "stat", permissionTier: "read-only" as const }, - { name: "du", permissionTier: "read-only" as const }, - { name: "dircolors", permissionTier: "read-only" as const }, - { name: "readlink", permissionTier: "read-only" as const }, - { name: "realpath", permissionTier: "read-only" as const }, - { name: "pathchk", permissionTier: "read-only" as const }, - - // Text processing - { name: "tee", permissionTier: "read-write" as const }, - { name: "echo", permissionTier: "read-only" as const }, - { name: "printf", permissionTier: "read-only" as const }, - { name: "wc", permissionTier: "read-only" as const }, - { name: "sort", permissionTier: "full" as const }, - { name: "uniq", permissionTier: "read-only" as const }, - { name: "cut", permissionTier: "read-only" as const }, - { name: "tr", permissionTier: "read-only" as const }, - { name: "paste", permissionTier: "read-only" as const }, - { name: "comm", permissionTier: "read-only" as const }, - { name: "join", permissionTier: "read-only" as const }, - { name: "fold", permissionTier: "read-only" as const }, - { name: "expand", permissionTier: "read-only" as const }, - { name: "unexpand", permissionTier: "read-only" as const }, - { name: "nl", permissionTier: "read-only" as const }, - { name: "fmt", permissionTier: "read-only" as const }, - { name: "od", permissionTier: "read-only" as const }, - { name: "ptx", permissionTier: "read-only" as const }, - { name: "numfmt", permissionTier: "read-only" as const }, - { name: "column", permissionTier: "read-only" as const }, - { name: "rev", permissionTier: "read-only" as const }, - { name: "strings", permissionTier: "read-only" as const }, - { name: "tac", permissionTier: "read-only" as const }, - { name: "tsort", permissionTier: "read-only" as const }, - - // Math and sequences - { name: "seq", permissionTier: "read-only" as const }, - { name: "shuf", permissionTier: "read-only" as const }, - { name: "factor", permissionTier: "read-only" as const }, - { name: "expr", permissionTier: "read-only" as const }, - - // Test and logic - { name: "test", permissionTier: "read-only" as const }, - { name: "[", permissionTier: "read-only" as const, aliasOf: "test" }, - { name: "true", permissionTier: "read-only" as const }, - { name: "false", permissionTier: "read-only" as const }, - { name: "yes", permissionTier: "read-only" as const }, - - // System info - { name: "arch", permissionTier: "read-only" as const }, - { name: "date", permissionTier: "read-only" as const }, - { name: "nproc", permissionTier: "read-only" as const }, - { name: "uname", permissionTier: "read-only" as const }, - { name: "logname", permissionTier: "read-only" as const }, - { name: "whoami", permissionTier: "read-only" as const }, - { name: "printenv", permissionTier: "read-only" as const }, - { name: "pwd", permissionTier: "read-only" as const }, - { name: "basename", permissionTier: "read-only" as const }, - { name: "dirname", permissionTier: "read-only" as const }, - { name: "which", permissionTier: "read-only" as const }, - { name: "sleep", permissionTier: "full" as const }, - - // Checksums and encoding - { name: "md5sum", permissionTier: "read-only" as const }, - { name: "sha1sum", permissionTier: "read-only" as const }, - { name: "sha224sum", permissionTier: "read-only" as const }, - { name: "sha256sum", permissionTier: "read-only" as const }, - { name: "sha384sum", permissionTier: "read-only" as const }, - { name: "sha512sum", permissionTier: "read-only" as const }, - { name: "b2sum", permissionTier: "read-only" as const }, - { name: "cksum", permissionTier: "read-only" as const }, - { name: "sum", permissionTier: "read-only" as const }, - { name: "base32", permissionTier: "read-only" as const }, - { name: "base64", permissionTier: "read-only" as const }, - { name: "basenc", permissionTier: "read-only" as const }, - - // Stubs (unimplemented commands that return graceful errors) - { name: "_stubs", permissionTier: "read-only" as const }, - { name: "chcon", permissionTier: "read-only" as const, aliasOf: "_stubs" }, - { - name: "runcon", - permissionTier: "read-only" as const, - aliasOf: "_stubs", - }, - { name: "chgrp", permissionTier: "read-only" as const, aliasOf: "_stubs" }, - { name: "chown", permissionTier: "read-only" as const, aliasOf: "_stubs" }, - { - name: "chroot", - permissionTier: "read-only" as const, - aliasOf: "_stubs", - }, - { name: "df", permissionTier: "read-only" as const, aliasOf: "_stubs" }, - { - name: "groups", - permissionTier: "read-only" as const, - aliasOf: "_stubs", - }, - { name: "id", permissionTier: "read-only" as const, aliasOf: "_stubs" }, - { - name: "hostname", - permissionTier: "read-only" as const, - aliasOf: "_stubs", - }, - { - name: "hostid", - permissionTier: "read-only" as const, - aliasOf: "_stubs", - }, - { - name: "install", - permissionTier: "read-only" as const, - aliasOf: "_stubs", - }, - { name: "kill", permissionTier: "read-only" as const, aliasOf: "_stubs" }, - { - name: "mkfifo", - permissionTier: "read-only" as const, - aliasOf: "_stubs", - }, - { name: "mknod", permissionTier: "read-only" as const, aliasOf: "_stubs" }, - { name: "pinky", permissionTier: "read-only" as const, aliasOf: "_stubs" }, - { name: "who", permissionTier: "read-only" as const, aliasOf: "_stubs" }, - { name: "users", permissionTier: "read-only" as const, aliasOf: "_stubs" }, - { - name: "uptime", - permissionTier: "read-only" as const, - aliasOf: "_stubs", - }, - { name: "stty", permissionTier: "read-only" as const, aliasOf: "_stubs" }, - { name: "sync", permissionTier: "read-only" as const, aliasOf: "_stubs" }, - { name: "tty", permissionTier: "read-only" as const, aliasOf: "_stubs" }, - ], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "coreutils", dir } satisfies PackageDescriptor; diff --git a/registry/software/curl/bin/curl b/registry/software/curl/bin/curl new file mode 100755 index 000000000..daa1f5d81 Binary files /dev/null and b/registry/software/curl/bin/curl differ diff --git a/registry/software/curl/package.json b/registry/software/curl/package.json index 66055eea3..546bffe70 100644 --- a/registry/software/curl/package.json +++ b/registry/software/curl/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/curl/secure-exec-package.json b/registry/software/curl/secure-exec-package.json deleted file mode 100644 index 3a5b790bc..000000000 --- a/registry/software/curl/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/curl", - "type": "wasm", - "description": "curl HTTP client", - "aptName": "curl", - "source": "c" -} diff --git a/registry/software/curl/secure-exec-package.meta.json b/registry/software/curl/secure-exec-package.meta.json deleted file mode 100644 index 885c43edf..000000000 --- a/registry/software/curl/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "curl": { - "size": 971195, - "sizeGzip": 384345 - } - }, - "totalSize": 971195, - "totalSizeGzip": 384345 -} diff --git a/registry/software/curl/src/index.ts b/registry/software/curl/src/index.ts index aff7a27f2..29815c318 100644 --- a/registry/software/curl/src/index.ts +++ b/registry/software/curl/src/index.ts @@ -1,29 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { existsSync } from "node:fs"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); -const COMMAND_DIR = resolve(__dirname, "..", "wasm"); -const FALLBACK_COMMAND_DIR = resolve( - __dirname, - "..", - "..", - "..", - "native/target/wasm32-wasip1/release/commands", -); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "curl", - aptName: "curl", - description: "curl-compatible HTTP client", - source: "rust" as const, - commands: [{ name: "curl", permissionTier: "full" as const }], - get commandDir() { - return existsSync(COMMAND_DIR) || !existsSync(FALLBACK_COMMAND_DIR) - ? COMMAND_DIR - : FALLBACK_COMMAND_DIR; - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "curl", dir } satisfies PackageDescriptor; diff --git a/registry/software/diffutils/bin/diff b/registry/software/diffutils/bin/diff new file mode 100755 index 000000000..8af0d9a5b Binary files /dev/null and b/registry/software/diffutils/bin/diff differ diff --git a/registry/software/diffutils/package.json b/registry/software/diffutils/package.json index cc457c667..da4e02c5f 100644 --- a/registry/software/diffutils/package.json +++ b/registry/software/diffutils/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/diffutils/secure-exec-package.json b/registry/software/diffutils/secure-exec-package.json deleted file mode 100644 index 17c2b6b7e..000000000 --- a/registry/software/diffutils/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/diffutils", - "type": "wasm", - "description": "GNU diffutils (diff)", - "aptName": "diffutils", - "source": "rust" -} diff --git a/registry/software/diffutils/secure-exec-package.meta.json b/registry/software/diffutils/secure-exec-package.meta.json deleted file mode 100644 index acf36bc2a..000000000 --- a/registry/software/diffutils/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "diff": { - "size": 132941, - "sizeGzip": 57564 - } - }, - "totalSize": 132941, - "totalSizeGzip": 57564 -} diff --git a/registry/software/diffutils/src/index.ts b/registry/software/diffutils/src/index.ts index 54a7d0b42..eaa730a30 100644 --- a/registry/software/diffutils/src/index.ts +++ b/registry/software/diffutils/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "diffutils", - aptName: "diffutils", - description: "GNU diffutils (diff)", - source: "rust" as const, - commands: [{ name: "diff", permissionTier: "read-only" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "diffutils", dir } satisfies PackageDescriptor; diff --git a/registry/software/duckdb/package.json b/registry/software/duckdb/package.json index aa0151c9d..f1b4d8a0c 100644 --- a/registry/software/duckdb/package.json +++ b/registry/software/duckdb/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/duckdb/secure-exec-package.json b/registry/software/duckdb/secure-exec-package.json deleted file mode 100644 index a55ef865d..000000000 --- a/registry/software/duckdb/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/duckdb", - "type": "wasm", - "description": "DuckDB command-line interface", - "aptName": "duckdb", - "source": "c" -} diff --git a/registry/software/duckdb/src/index.ts b/registry/software/duckdb/src/index.ts index 2a6aaa96a..216f01fd3 100644 --- a/registry/software/duckdb/src/index.ts +++ b/registry/software/duckdb/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "duckdb", - aptName: "duckdb", - description: "DuckDB command-line interface", - source: "c" as const, - commands: [{ name: "duckdb", permissionTier: "read-write" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "duckdb", dir } satisfies PackageDescriptor; diff --git a/registry/software/everything/package.json b/registry/software/everything/package.json index 58776f349..ac70eb4c5 100644 --- a/registry/software/everything/package.json +++ b/registry/software/everything/package.json @@ -40,7 +40,7 @@ "@agentos-software/codex-cli": "workspace:*" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/fd/bin/fd b/registry/software/fd/bin/fd new file mode 100755 index 000000000..9579b8995 Binary files /dev/null and b/registry/software/fd/bin/fd differ diff --git a/registry/software/fd/package.json b/registry/software/fd/package.json index 3b8a7b4ec..e280176a5 100644 --- a/registry/software/fd/package.json +++ b/registry/software/fd/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/fd/secure-exec-package.json b/registry/software/fd/secure-exec-package.json deleted file mode 100644 index 4449610d6..000000000 --- a/registry/software/fd/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/fd", - "type": "wasm", - "description": "fd fast file finder", - "aptName": "fd-find", - "source": "rust" -} diff --git a/registry/software/fd/secure-exec-package.meta.json b/registry/software/fd/secure-exec-package.meta.json deleted file mode 100644 index 8656b9c67..000000000 --- a/registry/software/fd/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "fd": { - "size": 928652, - "sizeGzip": 338311 - } - }, - "totalSize": 928652, - "totalSizeGzip": 338311 -} diff --git a/registry/software/fd/src/index.ts b/registry/software/fd/src/index.ts index 5d8693297..fb89ae695 100644 --- a/registry/software/fd/src/index.ts +++ b/registry/software/fd/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "fd", - aptName: "fd-find", - description: "fd fast file finder", - source: "rust" as const, - commands: [{ name: "fd", permissionTier: "read-only" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "fd", dir } satisfies PackageDescriptor; diff --git a/registry/software/file/bin/file b/registry/software/file/bin/file new file mode 100755 index 000000000..3478e0d44 Binary files /dev/null and b/registry/software/file/bin/file differ diff --git a/registry/software/file/package.json b/registry/software/file/package.json index f3fa16166..cbee5c325 100644 --- a/registry/software/file/package.json +++ b/registry/software/file/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/file/secure-exec-package.json b/registry/software/file/secure-exec-package.json deleted file mode 100644 index 1451c3136..000000000 --- a/registry/software/file/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/file", - "type": "wasm", - "description": "file type detection", - "aptName": "file", - "source": "rust" -} diff --git a/registry/software/file/secure-exec-package.meta.json b/registry/software/file/secure-exec-package.meta.json deleted file mode 100644 index b3774ac28..000000000 --- a/registry/software/file/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "file": { - "size": 119477, - "sizeGzip": 51098 - } - }, - "totalSize": 119477, - "totalSizeGzip": 51098 -} diff --git a/registry/software/file/src/index.ts b/registry/software/file/src/index.ts index 472b7a297..834107633 100644 --- a/registry/software/file/src/index.ts +++ b/registry/software/file/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "file", - aptName: "file", - description: "file type detection", - source: "rust" as const, - commands: [{ name: "file", permissionTier: "read-only" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "file", dir } satisfies PackageDescriptor; diff --git a/registry/software/findutils/bin/find b/registry/software/findutils/bin/find new file mode 100755 index 000000000..db8a6d57c Binary files /dev/null and b/registry/software/findutils/bin/find differ diff --git a/registry/software/findutils/bin/xargs b/registry/software/findutils/bin/xargs new file mode 100755 index 000000000..537127eb8 Binary files /dev/null and b/registry/software/findutils/bin/xargs differ diff --git a/registry/software/findutils/package.json b/registry/software/findutils/package.json index cf41d3e8f..e16682c86 100644 --- a/registry/software/findutils/package.json +++ b/registry/software/findutils/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/findutils/secure-exec-package.json b/registry/software/findutils/secure-exec-package.json deleted file mode 100644 index c0f46b2f5..000000000 --- a/registry/software/findutils/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/findutils", - "type": "wasm", - "description": "GNU findutils (find, xargs)", - "aptName": "findutils", - "source": "rust" -} diff --git a/registry/software/findutils/secure-exec-package.meta.json b/registry/software/findutils/secure-exec-package.meta.json deleted file mode 100644 index d607c6ae1..000000000 --- a/registry/software/findutils/secure-exec-package.meta.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "artifacts": { - "find": { - "size": 918544, - "sizeGzip": 332328 - }, - "xargs": { - "size": 71716, - "sizeGzip": 30879 - } - }, - "totalSize": 990260, - "totalSizeGzip": 363207 -} diff --git a/registry/software/findutils/src/index.ts b/registry/software/findutils/src/index.ts index 4655a50b9..27a6c8da0 100644 --- a/registry/software/findutils/src/index.ts +++ b/registry/software/findutils/src/index.ts @@ -1,21 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "findutils", - aptName: "findutils", - description: "GNU findutils (find, xargs)", - source: "rust" as const, - commands: [ - { name: "find", permissionTier: "read-only" as const }, - { name: "xargs", permissionTier: "full" as const }, - ], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "findutils", dir } satisfies PackageDescriptor; diff --git a/registry/software/gawk/bin/awk b/registry/software/gawk/bin/awk new file mode 100755 index 000000000..a41a45bfc Binary files /dev/null and b/registry/software/gawk/bin/awk differ diff --git a/registry/software/gawk/package.json b/registry/software/gawk/package.json index a85cafd9c..3e682bec1 100644 --- a/registry/software/gawk/package.json +++ b/registry/software/gawk/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/gawk/secure-exec-package.json b/registry/software/gawk/secure-exec-package.json deleted file mode 100644 index 1b0cc105a..000000000 --- a/registry/software/gawk/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/gawk", - "type": "wasm", - "description": "GNU awk text processing", - "aptName": "gawk", - "source": "rust" -} diff --git a/registry/software/gawk/secure-exec-package.meta.json b/registry/software/gawk/secure-exec-package.meta.json deleted file mode 100644 index 85ed863fc..000000000 --- a/registry/software/gawk/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "awk": { - "size": 1160791, - "sizeGzip": 442767 - } - }, - "totalSize": 1160791, - "totalSizeGzip": 442767 -} diff --git a/registry/software/gawk/src/index.ts b/registry/software/gawk/src/index.ts index b7705f061..3d34e627d 100644 --- a/registry/software/gawk/src/index.ts +++ b/registry/software/gawk/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "gawk", - aptName: "gawk", - description: "GNU awk text processing", - source: "rust" as const, - commands: [{ name: "awk", permissionTier: "full" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "gawk", dir } satisfies PackageDescriptor; diff --git a/registry/software/git/bin/git b/registry/software/git/bin/git new file mode 100755 index 000000000..0e8215351 Binary files /dev/null and b/registry/software/git/bin/git differ diff --git a/registry/software/git/bin/git-remote-http b/registry/software/git/bin/git-remote-http new file mode 100755 index 000000000..0e8215351 Binary files /dev/null and b/registry/software/git/bin/git-remote-http differ diff --git a/registry/software/git/bin/git-remote-https b/registry/software/git/bin/git-remote-https new file mode 100755 index 000000000..0e8215351 Binary files /dev/null and b/registry/software/git/bin/git-remote-https differ diff --git a/registry/software/git/package.json b/registry/software/git/package.json index 4fa11dcfb..67fbfc9b6 100644 --- a/registry/software/git/package.json +++ b/registry/software/git/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/git/secure-exec-package.json b/registry/software/git/secure-exec-package.json deleted file mode 100644 index 969422821..000000000 --- a/registry/software/git/secure-exec-package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@agentos-software/git", - "type": "wasm", - "description": "git version control", - "aptName": "git", - "source": "rust", - "status": "active" -} diff --git a/registry/software/git/secure-exec-package.meta.json b/registry/software/git/secure-exec-package.meta.json deleted file mode 100644 index aa4c3d879..000000000 --- a/registry/software/git/secure-exec-package.meta.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "artifacts": { - "git": { - "size": 216875, - "sizeGzip": 93789 - }, - "git-remote-http": { - "size": 216875, - "sizeGzip": 93801 - }, - "git-remote-https": { - "size": 216875, - "sizeGzip": 93802 - } - }, - "totalSize": 650625, - "totalSizeGzip": 281392 -} diff --git a/registry/software/git/src/index.ts b/registry/software/git/src/index.ts index 9ad1d24fd..3fcc9937c 100644 --- a/registry/software/git/src/index.ts +++ b/registry/software/git/src/index.ts @@ -1,22 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "git", - aptName: "git", - description: "git version control", - source: "rust" as const, - commands: [ - { name: "git", permissionTier: "full" as const }, - { name: "git-remote-http", permissionTier: "full" as const }, - { name: "git-remote-https", permissionTier: "full" as const }, - ], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "git", dir } satisfies PackageDescriptor; diff --git a/registry/software/grep/bin/egrep b/registry/software/grep/bin/egrep new file mode 100755 index 000000000..846a3cc2c Binary files /dev/null and b/registry/software/grep/bin/egrep differ diff --git a/registry/software/grep/bin/fgrep b/registry/software/grep/bin/fgrep new file mode 100755 index 000000000..846a3cc2c Binary files /dev/null and b/registry/software/grep/bin/fgrep differ diff --git a/registry/software/grep/bin/grep b/registry/software/grep/bin/grep new file mode 100755 index 000000000..846a3cc2c Binary files /dev/null and b/registry/software/grep/bin/grep differ diff --git a/registry/software/grep/package.json b/registry/software/grep/package.json index acaa6ee19..35e30e36b 100644 --- a/registry/software/grep/package.json +++ b/registry/software/grep/package.json @@ -8,7 +8,7 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "bin" ], "exports": { ".": { @@ -17,11 +17,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/grep/secure-exec-package.json b/registry/software/grep/secure-exec-package.json deleted file mode 100644 index d68d97039..000000000 --- a/registry/software/grep/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/grep", - "type": "wasm", - "description": "GNU grep pattern matching (grep, egrep, fgrep)", - "aptName": "grep", - "source": "rust" -} diff --git a/registry/software/grep/secure-exec-package.meta.json b/registry/software/grep/secure-exec-package.meta.json deleted file mode 100644 index c9db838a7..000000000 --- a/registry/software/grep/secure-exec-package.meta.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "artifacts": { - "egrep": { - "size": 907842, - "sizeGzip": 327186 - }, - "fgrep": { - "size": 907842, - "sizeGzip": 327186 - }, - "grep": { - "size": 907842, - "sizeGzip": 327185 - } - }, - "totalSize": 2723526, - "totalSizeGzip": 981557 -} diff --git a/registry/software/grep/src/index.ts b/registry/software/grep/src/index.ts index f2135bc69..593f386b6 100644 --- a/registry/software/grep/src/index.ts +++ b/registry/software/grep/src/index.ts @@ -1,22 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "grep", - aptName: "grep", - description: "GNU grep pattern matching (grep, egrep, fgrep)", - source: "rust" as const, - commands: [ - { name: "grep", permissionTier: "read-only" as const }, - { name: "egrep", permissionTier: "read-only" as const, aliasOf: "grep" }, - { name: "fgrep", permissionTier: "read-only" as const, aliasOf: "grep" }, - ], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "grep", dir } satisfies PackageDescriptor; diff --git a/registry/software/gzip/bin/gunzip b/registry/software/gzip/bin/gunzip new file mode 100755 index 000000000..19d8440f0 Binary files /dev/null and b/registry/software/gzip/bin/gunzip differ diff --git a/registry/software/gzip/bin/gzip b/registry/software/gzip/bin/gzip new file mode 100755 index 000000000..19d8440f0 Binary files /dev/null and b/registry/software/gzip/bin/gzip differ diff --git a/registry/software/gzip/bin/zcat b/registry/software/gzip/bin/zcat new file mode 100755 index 000000000..19d8440f0 Binary files /dev/null and b/registry/software/gzip/bin/zcat differ diff --git a/registry/software/gzip/package.json b/registry/software/gzip/package.json index b19557c91..e0553128b 100644 --- a/registry/software/gzip/package.json +++ b/registry/software/gzip/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/gzip/secure-exec-package.json b/registry/software/gzip/secure-exec-package.json deleted file mode 100644 index b89da40ad..000000000 --- a/registry/software/gzip/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/gzip", - "type": "wasm", - "description": "GNU gzip compression (gzip, gunzip, zcat)", - "aptName": "gzip", - "source": "rust" -} diff --git a/registry/software/gzip/secure-exec-package.meta.json b/registry/software/gzip/secure-exec-package.meta.json deleted file mode 100644 index 15867bdf9..000000000 --- a/registry/software/gzip/secure-exec-package.meta.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "artifacts": { - "gunzip": { - "size": 133911, - "sizeGzip": 66246 - }, - "gzip": { - "size": 133911, - "sizeGzip": 66244 - }, - "zcat": { - "size": 133911, - "sizeGzip": 66244 - } - }, - "totalSize": 401733, - "totalSizeGzip": 198734 -} diff --git a/registry/software/gzip/src/index.ts b/registry/software/gzip/src/index.ts index a7ffb6cb8..1ee80ab2a 100644 --- a/registry/software/gzip/src/index.ts +++ b/registry/software/gzip/src/index.ts @@ -1,22 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; +import type { PackageDescriptor } from "@agentos-software/manifest"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "gzip", - aptName: "gzip", - description: "GNU gzip compression (gzip, gunzip, zcat)", - source: "rust" as const, - commands: [ - { name: "gzip", permissionTier: "read-write" as const }, - { name: "gunzip", permissionTier: "read-write" as const, aliasOf: "gzip" }, - { name: "zcat", permissionTier: "read-only" as const, aliasOf: "gzip" }, - ], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "gzip", dir } satisfies PackageDescriptor; diff --git a/registry/software/http-get/package.json b/registry/software/http-get/package.json index 81f6f70a5..250972b34 100644 --- a/registry/software/http-get/package.json +++ b/registry/software/http-get/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/http-get/secure-exec-package.json b/registry/software/http-get/secure-exec-package.json deleted file mode 100644 index f3e32ebe3..000000000 --- a/registry/software/http-get/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/http-get", - "type": "wasm", - "description": "Minimal HTTP GET fetch helper", - "aptName": "http-get", - "source": "c" -} diff --git a/registry/software/http-get/secure-exec-package.meta.json b/registry/software/http-get/secure-exec-package.meta.json deleted file mode 100644 index c0bb4edfc..000000000 --- a/registry/software/http-get/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "http_get": { - "size": 35636, - "sizeGzip": 16161 - } - }, - "totalSize": 35636, - "totalSizeGzip": 16161 -} diff --git a/registry/software/http-get/src/index.ts b/registry/software/http-get/src/index.ts index 26f54309e..7fdab5386 100644 --- a/registry/software/http-get/src/index.ts +++ b/registry/software/http-get/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "http-get", - aptName: "http-get", - description: "Minimal HTTP GET fetch helper", - source: "c" as const, - commands: [{ name: "http_get", permissionTier: "full" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "http-get", dir } satisfies PackageDescriptor; diff --git a/registry/software/jq/bin/jq b/registry/software/jq/bin/jq new file mode 100755 index 000000000..f4494d352 Binary files /dev/null and b/registry/software/jq/bin/jq differ diff --git a/registry/software/jq/package.json b/registry/software/jq/package.json index cb518cf97..226839a2f 100644 --- a/registry/software/jq/package.json +++ b/registry/software/jq/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/jq/secure-exec-package.json b/registry/software/jq/secure-exec-package.json deleted file mode 100644 index cf989671d..000000000 --- a/registry/software/jq/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/jq", - "type": "wasm", - "description": "jq JSON processor", - "aptName": "jq", - "source": "rust" -} diff --git a/registry/software/jq/secure-exec-package.meta.json b/registry/software/jq/secure-exec-package.meta.json deleted file mode 100644 index 2068b3e55..000000000 --- a/registry/software/jq/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "jq": { - "size": 718311, - "sizeGzip": 305640 - } - }, - "totalSize": 718311, - "totalSizeGzip": 305640 -} diff --git a/registry/software/jq/src/index.ts b/registry/software/jq/src/index.ts index 20aa222d9..db04e760b 100644 --- a/registry/software/jq/src/index.ts +++ b/registry/software/jq/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "jq", - aptName: "jq", - description: "jq JSON processor", - source: "rust" as const, - commands: [{ name: "jq", permissionTier: "read-only" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "jq", dir } satisfies PackageDescriptor; diff --git a/registry/software/make/package.json b/registry/software/make/package.json index e5d1e0176..2982f510f 100644 --- a/registry/software/make/package.json +++ b/registry/software/make/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/make/secure-exec-package.json b/registry/software/make/secure-exec-package.json deleted file mode 100644 index c2b9668f9..000000000 --- a/registry/software/make/secure-exec-package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@agentos-software/make", - "type": "wasm", - "description": "GNU make build tool (planned)", - "aptName": "make", - "source": "rust", - "status": "planned" -} diff --git a/registry/software/make/src/index.ts b/registry/software/make/src/index.ts index 358ac0edf..a74f89479 100644 --- a/registry/software/make/src/index.ts +++ b/registry/software/make/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "make", - aptName: "make", - description: "GNU make build tool (planned)", - source: "rust" as const, - commands: [{ name: "make", permissionTier: "full" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "make", dir } satisfies PackageDescriptor; diff --git a/registry/software/ripgrep/bin/rg b/registry/software/ripgrep/bin/rg new file mode 100755 index 000000000..cb868d57f Binary files /dev/null and b/registry/software/ripgrep/bin/rg differ diff --git a/registry/software/ripgrep/package.json b/registry/software/ripgrep/package.json index 827423258..71f4a7849 100644 --- a/registry/software/ripgrep/package.json +++ b/registry/software/ripgrep/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/ripgrep/secure-exec-package.json b/registry/software/ripgrep/secure-exec-package.json deleted file mode 100644 index 36c3eaf86..000000000 --- a/registry/software/ripgrep/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/ripgrep", - "type": "wasm", - "description": "ripgrep fast recursive search", - "aptName": "ripgrep", - "source": "rust" -} diff --git a/registry/software/ripgrep/secure-exec-package.meta.json b/registry/software/ripgrep/secure-exec-package.meta.json deleted file mode 100644 index 688270325..000000000 --- a/registry/software/ripgrep/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "rg": { - "size": 950128, - "sizeGzip": 343963 - } - }, - "totalSize": 950128, - "totalSizeGzip": 343963 -} diff --git a/registry/software/ripgrep/src/index.ts b/registry/software/ripgrep/src/index.ts index 65faed1ed..b77821d5a 100644 --- a/registry/software/ripgrep/src/index.ts +++ b/registry/software/ripgrep/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; +import type { PackageDescriptor } from "@agentos-software/manifest"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "ripgrep", - aptName: "ripgrep", - description: "ripgrep fast recursive search", - source: "rust" as const, - commands: [{ name: "rg", permissionTier: "read-only" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "ripgrep", dir } satisfies PackageDescriptor; diff --git a/registry/software/sed/bin/sed b/registry/software/sed/bin/sed new file mode 100755 index 000000000..5586e4203 Binary files /dev/null and b/registry/software/sed/bin/sed differ diff --git a/registry/software/sed/package.json b/registry/software/sed/package.json index f26a0777a..2438437eb 100644 --- a/registry/software/sed/package.json +++ b/registry/software/sed/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/sed/secure-exec-package.json b/registry/software/sed/secure-exec-package.json deleted file mode 100644 index 360c55ef1..000000000 --- a/registry/software/sed/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/sed", - "type": "wasm", - "description": "GNU sed stream editor", - "aptName": "sed", - "source": "rust" -} diff --git a/registry/software/sed/secure-exec-package.meta.json b/registry/software/sed/secure-exec-package.meta.json deleted file mode 100644 index dcf58ea0c..000000000 --- a/registry/software/sed/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "sed": { - "size": 1251959, - "sizeGzip": 465372 - } - }, - "totalSize": 1251959, - "totalSizeGzip": 465372 -} diff --git a/registry/software/sed/src/index.ts b/registry/software/sed/src/index.ts index fa60b8a9a..25d97c20a 100644 --- a/registry/software/sed/src/index.ts +++ b/registry/software/sed/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "sed", - aptName: "sed", - description: "GNU sed stream editor", - source: "rust" as const, - commands: [{ name: "sed", permissionTier: "read-only" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "sed", dir } satisfies PackageDescriptor; diff --git a/registry/software/sqlite3/package.json b/registry/software/sqlite3/package.json index 81ed2c8fd..4acfe2452 100644 --- a/registry/software/sqlite3/package.json +++ b/registry/software/sqlite3/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/sqlite3/secure-exec-package.json b/registry/software/sqlite3/secure-exec-package.json deleted file mode 100644 index 0df66860c..000000000 --- a/registry/software/sqlite3/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/sqlite3", - "type": "wasm", - "description": "SQLite3 command-line interface", - "aptName": "sqlite3", - "source": "c" -} diff --git a/registry/software/sqlite3/secure-exec-package.meta.json b/registry/software/sqlite3/secure-exec-package.meta.json deleted file mode 100644 index 783218f40..000000000 --- a/registry/software/sqlite3/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "sqlite3": { - "size": 648819, - "sizeGzip": 288562 - } - }, - "totalSize": 648819, - "totalSizeGzip": 288562 -} diff --git a/registry/software/sqlite3/src/index.ts b/registry/software/sqlite3/src/index.ts index 4d9515f7e..519dabebe 100644 --- a/registry/software/sqlite3/src/index.ts +++ b/registry/software/sqlite3/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "sqlite3", - aptName: "sqlite3", - description: "SQLite3 command-line interface", - source: "c" as const, - commands: [{ name: "sqlite3", permissionTier: "read-write" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "sqlite3", dir } satisfies PackageDescriptor; diff --git a/registry/software/tar/bin/tar b/registry/software/tar/bin/tar new file mode 100755 index 000000000..3928681a9 Binary files /dev/null and b/registry/software/tar/bin/tar differ diff --git a/registry/software/tar/package.json b/registry/software/tar/package.json index 2c9ae6a2d..7bc4b701a 100644 --- a/registry/software/tar/package.json +++ b/registry/software/tar/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/tar/secure-exec-package.json b/registry/software/tar/secure-exec-package.json deleted file mode 100644 index 7bcf92ec4..000000000 --- a/registry/software/tar/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/tar", - "type": "wasm", - "description": "GNU tar archiver", - "aptName": "tar", - "source": "rust" -} diff --git a/registry/software/tar/secure-exec-package.meta.json b/registry/software/tar/secure-exec-package.meta.json deleted file mode 100644 index 5a441d7b8..000000000 --- a/registry/software/tar/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "tar": { - "size": 190969, - "sizeGzip": 89994 - } - }, - "totalSize": 190969, - "totalSizeGzip": 89994 -} diff --git a/registry/software/tar/src/index.ts b/registry/software/tar/src/index.ts index af8972ae5..60e316d51 100644 --- a/registry/software/tar/src/index.ts +++ b/registry/software/tar/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; +import type { PackageDescriptor } from "@agentos-software/manifest"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "tar", - aptName: "tar", - description: "GNU tar archiver", - source: "rust" as const, - commands: [{ name: "tar", permissionTier: "read-write" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "tar", dir } satisfies PackageDescriptor; diff --git a/registry/software/tree/bin/tree b/registry/software/tree/bin/tree new file mode 100755 index 000000000..ede3a1127 Binary files /dev/null and b/registry/software/tree/bin/tree differ diff --git a/registry/software/tree/package.json b/registry/software/tree/package.json index 498907d6e..6d0952da3 100644 --- a/registry/software/tree/package.json +++ b/registry/software/tree/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/tree/secure-exec-package.json b/registry/software/tree/secure-exec-package.json deleted file mode 100644 index 9ba00fe47..000000000 --- a/registry/software/tree/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/tree", - "type": "wasm", - "description": "tree directory listing", - "aptName": "tree", - "source": "rust" -} diff --git a/registry/software/tree/secure-exec-package.meta.json b/registry/software/tree/secure-exec-package.meta.json deleted file mode 100644 index 3a6a9504a..000000000 --- a/registry/software/tree/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "tree": { - "size": 69085, - "sizeGzip": 31406 - } - }, - "totalSize": 69085, - "totalSizeGzip": 31406 -} diff --git a/registry/software/tree/src/index.ts b/registry/software/tree/src/index.ts index 7a0434fd6..841806e00 100644 --- a/registry/software/tree/src/index.ts +++ b/registry/software/tree/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "tree", - aptName: "tree", - description: "tree directory listing", - source: "rust" as const, - commands: [{ name: "tree", permissionTier: "read-only" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "tree", dir } satisfies PackageDescriptor; diff --git a/registry/software/unzip/bin/unzip b/registry/software/unzip/bin/unzip new file mode 100755 index 000000000..7894ca8ad Binary files /dev/null and b/registry/software/unzip/bin/unzip differ diff --git a/registry/software/unzip/package.json b/registry/software/unzip/package.json index 19df946c9..efb828965 100644 --- a/registry/software/unzip/package.json +++ b/registry/software/unzip/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/unzip/secure-exec-package.json b/registry/software/unzip/secure-exec-package.json deleted file mode 100644 index f26590e27..000000000 --- a/registry/software/unzip/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/unzip", - "type": "wasm", - "description": "unzip archive extraction", - "aptName": "unzip", - "source": "c" -} diff --git a/registry/software/unzip/secure-exec-package.meta.json b/registry/software/unzip/secure-exec-package.meta.json deleted file mode 100644 index 13992895f..000000000 --- a/registry/software/unzip/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "unzip": { - "size": 71818, - "sizeGzip": 33018 - } - }, - "totalSize": 71818, - "totalSizeGzip": 33018 -} diff --git a/registry/software/unzip/src/index.ts b/registry/software/unzip/src/index.ts index 8af96f189..109c02a36 100644 --- a/registry/software/unzip/src/index.ts +++ b/registry/software/unzip/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; +import type { PackageDescriptor } from "@agentos-software/manifest"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "unzip", - aptName: "unzip", - description: "unzip archive extraction", - source: "c" as const, - commands: [{ name: "unzip", permissionTier: "read-only" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "unzip", dir } satisfies PackageDescriptor; diff --git a/registry/software/wget/package.json b/registry/software/wget/package.json index 975de8ca2..46d338f11 100644 --- a/registry/software/wget/package.json +++ b/registry/software/wget/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/wget/secure-exec-package.json b/registry/software/wget/secure-exec-package.json deleted file mode 100644 index 7ad17aada..000000000 --- a/registry/software/wget/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/wget", - "type": "wasm", - "description": "GNU wget HTTP client", - "aptName": "wget", - "source": "c" -} diff --git a/registry/software/wget/src/index.ts b/registry/software/wget/src/index.ts index 147ff723e..717ebb9a7 100644 --- a/registry/software/wget/src/index.ts +++ b/registry/software/wget/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "wget", - aptName: "wget", - description: "GNU wget HTTP client", - source: "c" as const, - commands: [{ name: "wget", permissionTier: "full" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "wget", dir } satisfies PackageDescriptor; diff --git a/registry/software/yq/bin/yq b/registry/software/yq/bin/yq new file mode 100755 index 000000000..81a77f103 Binary files /dev/null and b/registry/software/yq/bin/yq differ diff --git a/registry/software/yq/package.json b/registry/software/yq/package.json index 987f257fe..2063e2642 100644 --- a/registry/software/yq/package.json +++ b/registry/software/yq/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/yq/secure-exec-package.json b/registry/software/yq/secure-exec-package.json deleted file mode 100644 index fdb863ed0..000000000 --- a/registry/software/yq/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/yq", - "type": "wasm", - "description": "yq YAML/JSON processor", - "aptName": "yq", - "source": "rust" -} diff --git a/registry/software/yq/secure-exec-package.meta.json b/registry/software/yq/secure-exec-package.meta.json deleted file mode 100644 index 7387d4489..000000000 --- a/registry/software/yq/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "yq": { - "size": 965635, - "sizeGzip": 408897 - } - }, - "totalSize": 965635, - "totalSizeGzip": 408897 -} diff --git a/registry/software/yq/src/index.ts b/registry/software/yq/src/index.ts index 97834201f..d065a50df 100644 --- a/registry/software/yq/src/index.ts +++ b/registry/software/yq/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; -import { resolve, dirname } from "node:path"; +import type { PackageDescriptor } from "@agentos-software/manifest"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "yq", - aptName: "yq", - description: "yq YAML/JSON processor", - source: "rust" as const, - commands: [{ name: "yq", permissionTier: "read-only" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "yq", dir } satisfies PackageDescriptor; diff --git a/registry/software/zip/package.json b/registry/software/zip/package.json index 210f70dee..f09692447 100644 --- a/registry/software/zip/package.json +++ b/registry/software/zip/package.json @@ -8,7 +8,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "wasm" + "dist/package", + "bin" ], "exports": { ".": { @@ -17,11 +18,11 @@ } }, "scripts": { - "build": "tsc", + "build": "tsc && node ../../scripts/assemble-dist-package.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", "@types/node": "^22.10.2", "typescript": "^5.9.2" } diff --git a/registry/software/zip/secure-exec-package.json b/registry/software/zip/secure-exec-package.json deleted file mode 100644 index 9764cbfca..000000000 --- a/registry/software/zip/secure-exec-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@agentos-software/zip", - "type": "wasm", - "description": "zip archive creation", - "aptName": "zip", - "source": "c" -} diff --git a/registry/software/zip/secure-exec-package.meta.json b/registry/software/zip/secure-exec-package.meta.json deleted file mode 100644 index 813752ca8..000000000 --- a/registry/software/zip/secure-exec-package.meta.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "artifacts": { - "zip": { - "size": 80443, - "sizeGzip": 34531 - } - }, - "totalSize": 80443, - "totalSizeGzip": 34531 -} diff --git a/registry/software/zip/src/index.ts b/registry/software/zip/src/index.ts index 40b5be76e..747b806e5 100644 --- a/registry/software/zip/src/index.ts +++ b/registry/software/zip/src/index.ts @@ -1,18 +1,7 @@ -import type { WasmCommandPackage } from "@secure-exec/registry-types"; +import type { PackageDescriptor } from "@agentos-software/manifest"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; -const __dirname = dirname(fileURLToPath(import.meta.url)); +const dir = resolve(dirname(fileURLToPath(import.meta.url)), "package"); -const pkg = { - name: "zip", - aptName: "zip", - description: "zip archive creation", - source: "c" as const, - commands: [{ name: "zip", permissionTier: "read-write" as const }], - get commandDir() { - return resolve(__dirname, "..", "wasm"); - }, -} satisfies WasmCommandPackage; - -export default pkg; +export default { name: "zip", dir } satisfies PackageDescriptor; diff --git a/scripts/check-registry-software-split.mjs b/scripts/check-registry-software-split.mjs index 7e35f210f..4b79cc1f7 100644 --- a/scripts/check-registry-software-split.mjs +++ b/scripts/check-registry-software-split.mjs @@ -60,15 +60,12 @@ export function checkRegistrySoftwareSplit(options = {}) { const root = resolve(options.root ?? defaultRoot); const errors = []; + // package.json is the sole package manifest: the legacy + // secure-exec-package.json metadata file was removed in the registry split + // refactor, so this check reads name + dependency info from package.json only. for (const packageDir of collectSoftwareDirs(root)) { const dirName = packageDir.split(/[\\/]/).at(-1); const manifestPath = join(packageDir, "package.json"); - const metadataPath = join(packageDir, "secure-exec-package.json"); - const staleMetadataPath = join(packageDir, "agentos-package.json"); - const staleArtifactMetadataPath = join( - packageDir, - "agentos-package.meta.json", - ); const manifest = readJson(manifestPath); const expectedName = `@agentos-software/${dirName}`; @@ -78,27 +75,6 @@ export function checkRegistrySoftwareSplit(options = {}) { ); } - if (existsSync(staleMetadataPath)) { - errors.push( - `${formatPath(root, staleMetadataPath)} must be renamed to secure-exec-package.json`, - ); - } - if (existsSync(staleArtifactMetadataPath)) { - errors.push( - `${formatPath(root, staleArtifactMetadataPath)} must be renamed to secure-exec-package.meta.json`, - ); - } - if (!existsSync(metadataPath)) { - errors.push(`${formatPath(root, metadataPath)} is required`); - } else { - const metadata = readJson(metadataPath); - if (metadata.name !== manifest.name) { - errors.push( - `${formatPath(root, metadataPath)} name must match package.json (${manifest.name}), found ${metadata.name}`, - ); - } - } - checkDependencies(manifest.name ?? expectedName, manifest, errors); } diff --git a/scripts/check-registry-software-split.test.mjs b/scripts/check-registry-software-split.test.mjs index ad7b62070..6640c154e 100644 --- a/scripts/check-registry-software-split.test.mjs +++ b/scripts/check-registry-software-split.test.mjs @@ -25,45 +25,22 @@ test("accepts agentos-software registry software package metadata", () => { writeJson(root, "registry/software/coreutils/package.json", { name: "@agentos-software/coreutils", dependencies: { - "@secure-exec/registry-types": "workspace:*", + "@agentos-software/manifest": "workspace:*", }, }); - writeJson(root, "registry/software/coreutils/secure-exec-package.json", { - name: "@agentos-software/coreutils", - }); assert.deepEqual(checkRegistrySoftwareSplit({ root }), []); }); }); -test("rejects stale Agent OS package names and metadata files", () => { +test("rejects package names that do not match the directory", () => { withFixture((root) => { writeJson(root, "registry/software/grep/package.json", { name: "@rivet-dev/agent-os-pkg-grep", }); - writeJson(root, "registry/software/grep/agentos-package.json", { - name: "@rivet-dev/agent-os-pkg-grep", - }); assert.deepEqual(checkRegistrySoftwareSplit({ root }), [ "registry/software/grep/package.json must be named @agentos-software/grep, found @rivet-dev/agent-os-pkg-grep", - "registry/software/grep/agentos-package.json must be renamed to secure-exec-package.json", - "registry/software/grep/secure-exec-package.json is required", - ]); - }); -}); - -test("rejects metadata name drift", () => { - withFixture((root) => { - writeJson(root, "registry/software/sed/package.json", { - name: "@agentos-software/sed", - }); - writeJson(root, "registry/software/sed/secure-exec-package.json", { - name: "@agentos-software/grep", - }); - - assert.deepEqual(checkRegistrySoftwareSplit({ root }), [ - "registry/software/sed/secure-exec-package.json name must match package.json (@agentos-software/sed), found @agentos-software/grep", ]); }); }); @@ -76,9 +53,6 @@ test("rejects Agent OS dependencies inside software manifests", () => { "@rivet-dev/agent-os-core": "workspace:*", }, }); - writeJson(root, "registry/software/common/secure-exec-package.json", { - name: "@agentos-software/common", - }); assert.deepEqual(checkRegistrySoftwareSplit({ root }), [ "@agentos-software/common must not depend on Agent OS package @rivet-dev/agent-os-core in registry software dependencies", diff --git a/scripts/ci.sh b/scripts/ci.sh index 833c04c04..57251a52b 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -21,9 +21,15 @@ run_step() { "$@" } -run_step pnpm install --frozen-lockfile -run_step pnpm build -run_step pnpm check-types +# The website consumes the shared docs theme via `file:vendor/theme`, a gitignored +# local symlink that is only materialized for deploys — not in CI. Exclude the +# `@secure-exec/website` package from install/build/type-check/test so CI does not +# require the vendor (mirrors the publish workflow's `--filter`). +WEBSITE_FILTER="--filter=!@secure-exec/website" + +run_step pnpm install --frozen-lockfile "$WEBSITE_FILTER" +run_step pnpm exec turbo run build "$WEBSITE_FILTER" +run_step pnpm exec turbo run check-types "$WEBSITE_FILTER" run_step node --test scripts/check-secure-exec-boundary.test.mjs run_step node scripts/check-secure-exec-boundary.mjs run_step node --test scripts/check-no-escaping-local-deps.test.mjs @@ -35,7 +41,7 @@ run_step cargo clippy --workspace --all-targets -- -D warnings run_step env CARGO_INCREMENTAL=0 cargo test --workspace -- --test-threads=1 if [[ "${CI_FORK_PULL_REQUEST:-0}" == "1" ]]; then - run_step pnpm exec turbo run test --concurrency=1 + run_step pnpm exec turbo run test --concurrency=1 "$WEBSITE_FILTER" else - run_step env SECURE_EXEC_E2E_NETWORK=1 pnpm exec turbo run test --concurrency=1 + run_step env SECURE_EXEC_E2E_NETWORK=1 pnpm exec turbo run test --concurrency=1 "$WEBSITE_FILTER" fi diff --git a/scripts/publish/src/lib/packages.test.ts b/scripts/publish/src/lib/packages.test.ts index 363653e1f..c28310318 100644 --- a/scripts/publish/src/lib/packages.test.ts +++ b/scripts/publish/src/lib/packages.test.ts @@ -37,7 +37,6 @@ function writeSecureExecWorkspace(root: string) { [ "packages:", " - packages/*", - " - registry/file-system/*", " - registry/tool/*", "", ].join("\n"), @@ -50,9 +49,7 @@ function writeSecureExecWorkspace(root: string) { `packages/sidecar/npm/${platform}`, `@secure-exec/sidecar-${platform}`, ]), - ["packages/registry-types", "@secure-exec/registry-types"], - ["registry/file-system/s3", "@secure-exec/s3"], - ["registry/file-system/google-drive", "@secure-exec/google-drive"], + ["packages/registry-types", "@agentos-software/manifest"], ["registry/tool/sandbox", "@secure-exec/sandbox"], ]) { writeJson(root, join(rel, "package.json"), { diff --git a/scripts/publish/src/lib/packages.ts b/scripts/publish/src/lib/packages.ts index 3f7bd13c5..a86987c52 100644 --- a/scripts/publish/src/lib/packages.ts +++ b/scripts/publish/src/lib/packages.ts @@ -42,9 +42,7 @@ export const SECURE_EXEC_WORKSPACE_PACKAGES = new Set([ "secure-exec", "@secure-exec/browser", "@secure-exec/core", - "@secure-exec/google-drive", - "@secure-exec/registry-types", - "@secure-exec/s3", + "@agentos-software/manifest", "@secure-exec/sandbox", "@secure-exec/sidecar", "@secure-exec/typescript", @@ -157,9 +155,7 @@ export function assertDiscoverySanity(packages: Package[]): void { const required = [ "@secure-exec/browser", "@secure-exec/core", - "@secure-exec/google-drive", - "@secure-exec/registry-types", - "@secure-exec/s3", + "@agentos-software/manifest", "@secure-exec/sandbox", "@secure-exec/sidecar", ]; diff --git a/scripts/publish/src/lib/version.test.ts b/scripts/publish/src/lib/version.test.ts index 9e8783a02..aa7c04964 100644 --- a/scripts/publish/src/lib/version.test.ts +++ b/scripts/publish/src/lib/version.test.ts @@ -64,7 +64,6 @@ test("bumpPackageJsons injects secure-exec sidecar platform optional dependency" "packages:", " - packages/*", " - packages/sidecar/npm/*", - " - registry/file-system/*", " - registry/tool/*", "", ].join("\n"), @@ -72,14 +71,12 @@ test("bumpPackageJsons injects secure-exec sidecar platform optional dependency" for (const [rel, name] of [ ["packages/core", "@secure-exec/core"], ["packages/browser", "@secure-exec/browser"], - ["packages/registry-types", "@secure-exec/registry-types"], + ["packages/registry-types", "@agentos-software/manifest"], ["packages/sidecar", "@secure-exec/sidecar"], ...DEFAULT_SIDECAR_PLATFORMS.map((platform) => [ `packages/sidecar/npm/${platform}`, `@secure-exec/sidecar-${platform}`, ]), - ["registry/file-system/s3", "@secure-exec/s3"], - ["registry/file-system/google-drive", "@secure-exec/google-drive"], ["registry/tool/sandbox", "@secure-exec/sandbox"], ]) { await writeJson(repoRoot, join(rel, "package.json"), {