diff --git a/crates/agentos-actor-plugin/src/actions/mod.rs b/crates/agentos-actor-plugin/src/actions/mod.rs index 9c9ce750df..b6ec0c37e1 100644 --- a/crates/agentos-actor-plugin/src/actions/mod.rs +++ b/crates/agentos-actor-plugin/src/actions/mod.rs @@ -114,6 +114,7 @@ fn reply_err(host: &HostCtx, token: u64, error: anyhow::Error) { pub(crate) async fn dispatch( host: &HostCtx, vm: &AgentOs, + config: &crate::config::AgentOsConfigJson, vars: &mut Vars, name: &str, args: &[u8], @@ -456,6 +457,23 @@ pub(crate) async fn dispatch( } Err(error) => reply_err(host, token, error), }, + // Config introspection: echo the actor's declarative mount / software + // config (no VM round-trip — the kernel has no runtime mount table and + // software is the requested bundle expanded TS-side in buildConfigJson). + "listMounts" => reply_ok(host, token, &config.list_mounts()), + "listSoftware" => { + // Config carries package/kind/version; the command names each + // wasm-commands package ships come from the live VM (host package + // dirs), zipped in here by package name. + let mut list = config.list_software(); + let commands: HashMap> = vm.provided_commands().into_iter().collect(); + for dto in &mut list { + if let Some(cmds) = commands.get(&dto.package) { + dto.commands = cmds.clone(); + } + } + reply_ok(host, token, &list); + } other => { host.reply_err( token, diff --git a/crates/agentos-actor-plugin/src/config.rs b/crates/agentos-actor-plugin/src/config.rs index 2757c20a12..0d99606330 100644 --- a/crates/agentos-actor-plugin/src/config.rs +++ b/crates/agentos-actor-plugin/src/config.rs @@ -81,6 +81,36 @@ struct SidecarJson { pool: Option, } +/// Reply DTO for the `listMounts` action: one configured mount, flattened so the +/// UI gets `path` / `kind` (the native plugin id, e.g. `host_dir`, `s3`, +/// `google_drive`, `sandbox_agent`) / `config` (provider-specific detail) / +/// `readOnly`. This echoes the actor's declarative mount config — the kernel +/// has no runtime mount table to enumerate. +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct MountInfoDto { + pub path: String, + pub kind: String, + pub config: Option, + pub read_only: bool, +} + +/// Reply DTO for the `listSoftware` action: one configured software package. +/// `kind` is the kebab-case [`SoftwareKind`] tag (`wasm-commands` / `agent` / +/// `tool`). Reflects the requested `software` bundle (the default `common` +/// bundle is already expanded into the envelope TS-side in `buildConfigJson`). +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct SoftwareInfoDto { + pub package: String, + pub kind: String, + pub version: Option, + /// Command names this package ships (wasm-commands packages only; empty for + /// agent/tool). Filled from the live VM in the `listSoftware` dispatch arm, + /// not derivable from config alone. See `AgentOs::provided_commands`. + pub commands: Vec, +} + impl AgentOsConfigJson { /// Parse a `config_json` envelope. An empty/whitespace string is treated as /// the default config (the client supplied no overrides). @@ -144,4 +174,37 @@ impl AgentOsConfigJson { ..AgentOsConfig::default() } } + + /// Configured mounts, flattened for the `listMounts` action. `kind` is the + /// native plugin id and `config` its provider-specific detail. + pub(crate) fn list_mounts(&self) -> Vec { + self.mounts + .iter() + .map(|mount| MountInfoDto { + path: mount.path.clone(), + kind: mount.plugin.id.clone(), + config: mount.plugin.config.clone(), + read_only: mount.read_only, + }) + .collect() + } + + /// Configured software packages, for the `listSoftware` action. `kind` is the + /// kebab-case [`SoftwareKind`] serde tag (`wasm-commands` / `agent` / `tool`). + pub(crate) fn list_software(&self) -> Vec { + self.software + .iter() + .map(|software| SoftwareInfoDto { + package: software.package.clone(), + kind: serde_json::to_value(software.kind) + .ok() + .and_then(|value| value.as_str().map(str::to_owned)) + .unwrap_or_else(|| "wasm-commands".to_owned()), + version: software.version.clone(), + // Filled from the live VM in the dispatch arm (config alone does + // not carry the packages' command binaries). + commands: Vec::new(), + }) + .collect() + } } diff --git a/crates/agentos-actor-plugin/src/lib.rs b/crates/agentos-actor-plugin/src/lib.rs index c321e37346..ac312efa20 100644 --- a/crates/agentos-actor-plugin/src/lib.rs +++ b/crates/agentos-actor-plugin/src/lib.rs @@ -361,8 +361,16 @@ async fn actor_worker( match abi::decode_action_payload(&payload) { Ok((name, action_args)) => { tracing::debug!(action = %name, "agent-os action start"); - actions::dispatch(&host, vm_ref, &mut vars, &name, &action_args, token) - .await; + actions::dispatch( + &host, + vm_ref, + &config, + &mut vars, + &name, + &action_args, + token, + ) + .await; tracing::debug!(action = %name, "agent-os action done"); } Err(_) => { diff --git a/crates/client/src/agent_os.rs b/crates/client/src/agent_os.rs index 48420cd1ab..5c1ccf38e7 100644 --- a/crates/client/src/agent_os.rs +++ b/crates/client/src/agent_os.rs @@ -834,6 +834,48 @@ impl AgentOs { pub fn sidecar(&self) -> Arc { self.inner.sidecar.clone() } + + /// The commands each configured `wasm-commands` package *ships*, keyed by + /// the package string used in the config — i.e. the wasm binaries in the + /// package's resolved command directory (`entry.descriptor.root`, which is + /// exactly what `build_command_mounts` mounts at `/__secure_exec/commands/ + /// {index}/`), with `_stubs`/dotfiles filtered, sorted. Non-`wasm-commands` + /// packages contribute nothing. + /// + /// WORKAROUND: agent-os owns command *provisioning* (it resolves each + /// package and mounts its dir), so it knows the host dirs and can read them + /// here. But the authoritative *resolved* command set — deduping when two + /// packages provide the same command, priority order, and which files are + /// actually executable — is owned by secure-exec's command discovery + /// (`discover_command_guest_paths`). This reads the raw package contents and + /// re-derives a slice of that. TODO: replace with a secure-exec API that + /// reports discovered commands per package instead of us re-reading dirs. + pub fn provided_commands(&self) -> Vec<(String, Vec)> { + let resolved = match resolve_software(&self.inner.config) { + Ok(resolved) => resolved, + Err(_) => return Vec::new(), + }; + resolved + .into_iter() + .filter(|entry| entry.kind == SoftwareKind::WasmCommands) + .map(|entry| { + // `root` IS the command directory (mounted verbatim as the + // guest command dir), so the command binaries live directly in + // it — do not descend into a `wasm/` subdir. + let mut commands: Vec = std::fs::read_dir(&entry.descriptor.root) + .into_iter() + .flatten() + .flatten() + .filter_map(|dirent| dirent.file_name().into_string().ok()) + // `_stubs` is a secure-exec support entry, not a command; + // dotfiles are metadata. Everything else is a command name. + .filter(|name| !name.starts_with('_') && !name.starts_with('.')) + .collect(); + commands.sort(); + (entry.descriptor.package_name, commands) + }) + .collect() + } } /// Abort and clear a single tracked background-task handle (e.g. the ACP event pump) so it cannot diff --git a/packages/agentos/.claude/scheduled_tasks.lock b/packages/agentos/.claude/scheduled_tasks.lock new file mode 100644 index 0000000000..9f49e2f2c2 --- /dev/null +++ b/packages/agentos/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"bb55ab39-b384-49a3-923e-20bf190e8d86","pid":2222620,"procStart":"42140112","acquiredAt":1782774961870} \ No newline at end of file diff --git a/packages/agentos/src/actor-actions.ts b/packages/agentos/src/actor-actions.ts index ce8d438b59..208870fc6e 100644 --- a/packages/agentos/src/actor-actions.ts +++ b/packages/agentos/src/actor-actions.ts @@ -113,6 +113,24 @@ export interface ReadFileResult { error?: string; } +/** One configured mount, returned by `listMounts`. Mirrors `MountInfoDto`. */ +export interface MountInfo { + path: string; + /** Native mount plugin id. */ + kind: "host_dir" | "s3" | "google_drive" | "sandbox_agent"; + /** Provider-specific config detail (null when the plugin carries none). */ + config: unknown; + readOnly: boolean; +} + +/** One configured software package, returned by `listSoftware`. Mirrors `SoftwareInfoDto`. */ +export interface SoftwareInfo { + package: string; + /** Kebab-case `SoftwareKind` tag. */ + kind: "wasm-commands" | "agent" | "tool"; + version: string | null; +} + /** * The agentOS VM actor's action map. Keep one method per Rust `dispatch` arm. * @@ -188,4 +206,8 @@ export type AgentOsActions = { // ── Preview URLs ────────────────────────────────────────────────── createSignedPreviewUrl: (c: Ctx, port: number, ttlSeconds: number) => Promise; expireSignedPreviewUrl: (c: Ctx, token: string) => Promise; + + // ── Config introspection ────────────────────────────────────────── + listMounts: (c: Ctx) => Promise; + listSoftware: (c: Ctx) => Promise; }