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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ adheres to [Semantic Versioning](https://semver.org/).
- **The example's approval gate** (`confirm-dangerous.ts`, used by `coding_agent` and `handoff_agent`) asks before selected operations that delete, discard or overwrite the owner's files. It tokenizes each command (quotes, escapes, redirections, heredoc bodies skipped) and reads it command by command. It follows `cd`, looks past wrappers (`env`, `timeout`, `nice`, `xargs`, …), reads git's subcommand past its global options, and checks `bash -c` scripts the same way. Anything with a path it cannot know (`$VAR`, `~`, globs, an unfollowable `cd`) asks. It covers `rm`, `git reset --hard`, `git clean`, `git checkout` / `restore` of paths, `git branch -D`, a forced push, `find -delete`, and a write, `mv`, `cp`, `tee` or redirection over an existing file. A guardrail, not a sandbox: targeted edits and what a script does inside are not checked. Before, it caught only `rm -r`, `sudo`, `chmod -R`, `git push --force`, `mkfs` and `dd`. Files the agent created this session, and the scratch folder a host names in `YOAGENT_SCRATCH`, are exempt. An ended session now ends its open questions with their default, where they used to be left open. Steering or a follow-up that arrives as a run ends (too late for that run to read) now starts the next run instead of waiting for whatever run comes next.
- Tool cards: a tool result's `details.view` is drawn as a card in the browser (and titles the tool line: in the browser when no UI plugin draws the tool, always in the terminal UI); questions gain `message` and multiple-choice `select` (options sent once each); the `ui` service gains `frontends()`; the terminal UI asks again on an answer that does not fit.

### yoagent-rutis (unreleased)

- **DSH's own host services for dsh plugins**: `plugins/dsh/host-services.json` lists the DSH packages that provide what many dsh plugins wait for — files, attachments, credentials, storage, session persistence, skills — and run headless; the compatibility probe loads them, the frontend examples on request (`--dsh-services`). Attachments, the credentials file and skills live under `DSH_HOME` (else `~/.dsh`) and skills also under `DSH_AGENTS_HOME` (else `~/.agents`), so hosts give the runtime homes of their own; that keeps `~/.dsh` and `~/.agents` out, **not** the inherited environment (DSH's credentials service reads it first), the project's `.env` or its skill folders, nor the filesystem (`dsh-fs-local` has no sandbox). In the [plugin compatibility](docs/evals/plugin-compatibility.md) matrix (re-measured 2026-10-12, on rutis 0.9) DSH's top 20 go from 3 to 4 usable (`@dfy-plugins/dsh-visualize`); what still blocks the rest is DSH's app — its web UI, model layer, agents and settings, among others. pi unchanged at 12/20.
- yoagent-frontend: `PluginHostBuilder::env(runtime, name, value)` (a runtime not started is an error), `host::DshHome` (`YOAGENT_DSH_HOME`, else a private owner-only directory per run) and `host::rows_from_file`; `coding_agent` / `handoff_agent` take `--dsh-services`.

## yoagent-rutis 0.3.0 (2026-10-12)

yoagent-rutis only; yoagent is unchanged (0.25.2). Requires yoagent 0.25, **rutis 0.9** and **Rust 1.88**.
Expand Down
1,314 changes: 1,011 additions & 303 deletions docs/evals/plugin-compatibility.json

Large diffs are not rendered by default.

74 changes: 37 additions & 37 deletions docs/evals/plugin-compatibility.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion integrations/yoagent-frontend/CLAUDE.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions integrations/yoagent-frontend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ publish = false
exclude = ["plugins/node_modules"]

[dependencies]
# DshHome: a private, per-run home for DSH's own services.
tempfile = "3"
yoagent = { path = "../.." }
yoagent-rutis = { path = "../yoagent-rutis", features = ["node"] }
rutis = "0.9"
Expand Down
27 changes: 26 additions & 1 deletion integrations/yoagent-frontend/examples/coding_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
//! (a question gets "no", as from a user who walked away). CI runs it.
//! - `--live`: DeepSeek (`DEEPSEEK_API_KEY`); otherwise a scripted model.
//! - `--no-dsh`: skip DSH (web search, `ask_user_question` and its dialogs).
//! - `--dsh-services`: also load DSH's own host services (files, attachments,
//! credentials, storage, session persistence, skills) for DSH plugins that
//! need them, in a DSH home of their own (`YOAGENT_DSH_HOME`, else private).
//! - `--watch`: reload a plugin when its file changes (polled twice a second):
//! the UI plugins, the adapters, the pi extension; each reload is told to
//! the frontends. New code reaches the next run. A version that does not
Expand All @@ -39,7 +42,7 @@ use yoagent::provider::mock::{MockResponse, MockToolCall};
use yoagent::provider::{MockProvider, ModelConfig};
use yoagent::tools::default_tools;
use yoagent::Agent;
use yoagent_frontend::host::{PluginHost, Row};
use yoagent_frontend::host::{rows_from_file, DshHome, PluginHost, Row};
use yoagent_frontend::{services, session, web, NoticeLevel, Session};
use yoagent_rutis::RutisBridge;

Expand All @@ -57,12 +60,15 @@ struct Options {
web: Option<SocketAddr>,
demo: Option<String>,
dsh: bool,
/// DSH's own host services (`--dsh-services`), for DSH plugins that need them.
dsh_services: bool,
watch: bool,
}

fn main() -> Result<(), BoxError> {
let (mut live, mut web, mut demo, mut watch) = (false, None, None, false);
let mut dsh = rutis_plugins().join("dsh/node_modules").exists();
let mut dsh_services = false;
let mut args = std::env::args().skip(1).peekable();
while let Some(arg) = args.next() {
match arg.as_str() {
Expand All @@ -76,6 +82,7 @@ fn main() -> Result<(), BoxError> {
}
"--demo" => demo = Some(args.next().ok_or("--demo needs a prompt")?),
"--no-dsh" => dsh = false,
"--dsh-services" => dsh_services = true,
"--watch" => watch = true,
other => return Err(format!("unknown argument {other}").into()),
}
Expand Down Expand Up @@ -103,6 +110,7 @@ fn main() -> Result<(), BoxError> {
web,
demo,
dsh,
dsh_services,
watch,
};
tokio::runtime::Builder::new_multi_thread()
Expand Down Expand Up @@ -161,8 +169,17 @@ async fn run(root: &Ctx, options: Options) -> Result<(), BoxError> {
let mut builder = PluginHost::builder()
.node("ui", here().join("plugins"))
.node("pi", rutis_plugins().join("pi"));
// DSH's own host services, on request: in a home of their own, never
// the user's `~/.dsh` (see `DshHome` for what that does not cover).
let dsh_home = match options.dsh && options.dsh_services {
true => Some(DshHome::new()?),
false => None,
};
if options.dsh {
builder = builder.node("dsh", rutis_plugins().join("dsh"));
for (name, value) in dsh_home.iter().flat_map(DshHome::env) {
builder = builder.env("dsh", name, value);
}
}
if options.web.is_none() && options.demo.is_none() {
// The terminal UI reads the keyboard: its runtime keeps the terminal.
Expand Down Expand Up @@ -200,6 +217,14 @@ async fn run(root: &Ctx, options: Options) -> Result<(), BoxError> {
.runtime("ui"),
];
if options.dsh {
// The services a headless DSH host runs, for plugins that need them
// (none of the rows below does).
if options.dsh_services {
rows.extend(rows_from_file(
&rutis_plugins().join("dsh/host-services.json"),
"dsh",
)?);
}
let dsh = |id: &str, name: &str| Row::new(id, name).runtime("dsh");
rows.extend([
dsh("dsh-web", "@deepseek-ai/dsh-web"),
Expand Down
25 changes: 23 additions & 2 deletions integrations/yoagent-frontend/examples/handoff_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
//! (`cargo run` needs `--manifest-path path/to/yoagent/integrations/yoagent-frontend/Cargo.toml`
//! when started from the project directory.)
//!
//! `--web ADDR` (default 127.0.0.1:8787), `--no-dsh`, `--max-minutes N`
//! `--web ADDR` (default 127.0.0.1:8787), `--no-dsh`, `--dsh-services` (DSH's own host services, for DSH plugins that need them, in a DSH home of their own), `--max-minutes N`
//! (30) and `--max-tokens N` (4M) per run, `--budget-usd X` (2; priced
//! models only). Without a task, give it
//! one in the page. `--phone` serves on every interface: anyone on the
Expand Down Expand Up @@ -57,7 +57,7 @@ use yoagent::{
Agent, AgentEvent, AgentMessage, AgentTool, Content, Message, ToolContext, ToolError,
ToolResult,
};
use yoagent_frontend::host::{PluginHost, Row};
use yoagent_frontend::host::{rows_from_file, DshHome, PluginHost, Row};
use yoagent_frontend::{
services, session, web, ClientMessage, NoticeLevel, ServerMessage, Session, UiRequest,
};
Expand Down Expand Up @@ -108,6 +108,8 @@ struct Options {
phone: bool,
demo: bool,
dsh: bool,
/// DSH's own host services (`--dsh-services`), for DSH plugins that need them.
dsh_services: bool,
task: Option<String>,
max_minutes: u64,
max_tokens: usize,
Expand All @@ -123,6 +125,7 @@ fn main() -> Result<(), BoxError> {
phone: false,
demo: false,
dsh: rutis_plugins().join("dsh/node_modules").exists(),
dsh_services: false,
task: None,
max_minutes: 30,
// Research reads a lot: yoagent's default (1M per run, the whole
Expand All @@ -139,6 +142,7 @@ fn main() -> Result<(), BoxError> {
"--live" => live = true,
"--demo" => options.demo = true,
"--no-dsh" => options.dsh = false,
"--dsh-services" => options.dsh_services = true,
"--phone" => {
options.phone = true;
options.addr = "0.0.0.0:8787".parse()?;
Expand Down Expand Up @@ -218,8 +222,17 @@ async fn run(root: &Ctx, options: Options) -> Result<(), BoxError> {
let mut builder = PluginHost::builder()
.node("ui", here().join("plugins"))
.node("pi", rutis_plugins().join("pi"));
// DSH's own host services, on request: in a home of their own, never
// the user's `~/.dsh` (see `DshHome` for what that does not cover).
let dsh_home = match options.dsh && options.dsh_services {
true => Some(DshHome::new()?),
false => None,
};
if options.dsh {
builder = builder.node("dsh", rutis_plugins().join("dsh"));
for (name, value) in dsh_home.iter().flat_map(DshHome::env) {
builder = builder.env("dsh", name, value);
}
}
let mut host = builder
.share("yoagent")
Expand Down Expand Up @@ -251,6 +264,14 @@ async fn run(root: &Ctx, options: Options) -> Result<(), BoxError> {
Row::new("todo-plan", here().join("plugins/todo-plan.ts").display()).runtime("ui"),
];
if options.dsh {
// The services a headless DSH host runs, for plugins that need them
// (none of the rows below does).
if options.dsh_services {
rows.extend(rows_from_file(
&rutis_plugins().join("dsh/host-services.json"),
"dsh",
)?);
}
let dsh = |id: &str, name: &str| Row::new(id, name).runtime("dsh");
rows.extend([
dsh("dsh-web", "@deepseek-ai/dsh-web"),
Expand Down
Loading
Loading