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
18 changes: 12 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -309,16 +309,16 @@ jobs:
- name: Check
run: cargo check --manifest-path "$MANIFEST" --all-targets
- name: Check with the language features (rutis-bridge 0.9)
run: cargo check --manifest-path "$MANIFEST" --all-targets --features node,python,bun,websocket
run: cargo check --manifest-path "$MANIFEST" --all-targets --features node,python,bun,go,websocket

# TypeScript (Node and Bun 1.4) and Python plugins through rutis-bridge 0.9, end to end: real
# TypeScript (Node and Bun 1.4), Python and Go (1.24) plugins through rutis-bridge 0.9, end to end: real
# Node 22 (the floor: pi needs 22.19, rutis 0.9 22; the frontend job runs 24)
# and Python 3.12 runtimes load the example plugins and fixtures as
# rutis-loader rows. The npm and PyPI packages are pinned to the 0.9.1
# train (plugins/package-lock.json, plugins/requirements.txt). A missing
# runtime fails the job instead of skipping (YOAGENT_RUTIS_REQUIRE_RUNTIMES).
rutis-bridge-languages:
name: yoagent-rutis TypeScript / Bun / Python plugins
name: yoagent-rutis TypeScript / Bun / Python / Go plugins
needs: changes
if: needs.changes.outputs.rutis == 'true'
runs-on: ubuntu-latest
Expand Down Expand Up @@ -352,6 +352,12 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.4"
# The Go plugin's test (tests/go_test.rs) builds plugins/go: the rutis
# Go SDK's floor.
- uses: actions/setup-go@v5
with:
go-version: "1.24"
cache-dependency-path: integrations/yoagent-rutis/plugins/go/go.sum
- name: Install the rutis npm packages
run: npm ci --prefix "$PLUGINS"
# The dsh adapter's end-to-end test (tests/dsh_test.rs): dsh-tools,
Expand All @@ -368,13 +374,13 @@ jobs:
"$PLUGINS/.venv/bin/pip" install -r "$PLUGINS/requirements.txt"
- name: Clippy (each language feature, and all)
run: |
for features in node python bun websocket node,python,bun,websocket; do
for features in node python bun go websocket node,python,bun,go,websocket; do
cargo clippy --manifest-path "$MANIFEST" --all-targets --features "$features"
done
- name: Documentation
run: RUSTDOCFLAGS="-Dwarnings" cargo doc --manifest-path "$MANIFEST" --no-deps --features node,python,bun,websocket
run: RUSTDOCFLAGS="-Dwarnings" cargo doc --manifest-path "$MANIFEST" --no-deps --features node,python,bun,go,websocket
- name: Test
run: cargo test --manifest-path "$MANIFEST" --features node,python,bun,websocket
run: cargo test --manifest-path "$MANIFEST" --features node,python,bun,go,websocket
- name: Example
run: cargo run --manifest-path "$MANIFEST" --features node,python --example language_plugins
- name: pi extensions example (scripted, the fixture extension)
Expand Down
2 changes: 1 addition & 1 deletion integrations/yoagent-rutis/CLAUDE.md

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions integrations/yoagent-rutis/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ tokio-util = "0.7"
tracing = "0.1"

[package.metadata.docs.rs]
features = ["node", "python", "bun", "websocket"]
features = ["node", "python", "bun", "go", "websocket"]
rustdoc-args = ["--cfg", "docsrs"]

[features]
Expand All @@ -57,6 +57,9 @@ rustdoc-args = ["--cfg", "docsrs"]
default = []
# TypeScript / JavaScript plugins in local Node runtimes (Unix).
node = ["rutis-bridge/node"]
# Go plugins (Go 1.24+, the rutis Go SDK): one binary per runtime, started
# when a row needs it (Unix).
go = ["rutis-bridge/go"]
# Python plugins in local Python runtimes (Unix).
python = ["rutis-bridge/python"]
# TypeScript / JavaScript plugins in local Bun runtimes (Bun 1.4+, Unix).
Expand All @@ -68,11 +71,15 @@ websocket = ["rutis-bridge/websocket"]
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "sync"] }
# Loads language plugins as rows in the end-to-end tests. The bridge never
# loads plugins itself: that is the host's job.
rutis-loader = { version = "0.9", features = ["node", "python", "bun"] }
rutis-loader = { version = "0.9", features = ["node", "python", "bun", "go"] }
tempfile = "3"
# Captures the bridge's `log` output in its own test binary (tests/plugin_log_test.rs).
tracing-subscriber = { version = "0.3", default-features = false, features = ["registry"] }

[[test]]
name = "go_test"
required-features = ["go"]

[[test]]
name = "languages_test"
required-features = ["node", "python"]
Expand Down
4 changes: 3 additions & 1 deletion integrations/yoagent-rutis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,16 @@ service `yoagent`:
| `node` | TypeScript / JavaScript plugins in local Node runtimes (Node 22.19+, Linux/macOS) |
| `bun` | the same TypeScript / JavaScript plugins in local Bun runtimes (Bun 1.4+, Linux/macOS): rows `bun:<module>`, no build step; local only — rutis's Bun runtime cannot listen remotely yet |
| `python` | Python plugins in local Python runtimes (Python 3.10+, Linux/macOS) |
| `go` | Go plugins built with the rutis Go SDK (Go 1.24+, Linux/macOS): each binary is a runtime, started when a `go:<plugin>` row needs it (rutis-loader's `GoResolver` / `GoRuntimes`). A handler is a map of Go functions; a hook's `context.Context` is cancelled when the host gives the call up, as a JavaScript hook's `signal` aborts — see [`plugins/go/main.go`](plugins/go/main.go) |
| `websocket` | runtimes and nodes on other machines, over `wss` |

```toml
yoagent-rutis = { git = "https://github.com/yologdev/yoagent", features = ["node", "python"] }
```

A plugin injects `yoagent` and registers a handler: an object (in Python, a
class instance or a dict) of async functions, passed by reference — rutis
class instance or a dict; in Go, a map of functions — see
[`plugins/go/main.go`](plugins/go/main.go)) of async functions, passed by reference — rutis
calls them in the plugin's own process. `register(name, handler, options?)`
returns a function that unregisters it; pass it to `ctx.effect` so the
handler goes when the plugin unloads.
Expand Down
5 changes: 5 additions & 0 deletions integrations/yoagent-rutis/plugins/go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/yologdev/yoagent/integrations/yoagent-rutis/plugins/go

go 1.24

require github.com/arcships/rutis/go/rutis v0.9.1
2 changes: 2 additions & 0 deletions integrations/yoagent-rutis/plugins/go/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/arcships/rutis/go/rutis v0.9.1 h1:tYFxAyMMce2L2RnaLgatamRYgP3RRElG7A3v/1xcwgs=
github.com/arcships/rutis/go/rutis v0.9.1/go.mod h1:cJscMrYVCQ5AaQJjM39IPkk15A6GnBLfpo6XiZ9PT2M=
116 changes: 116 additions & 0 deletions integrations/yoagent-rutis/plugins/go/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// A yoagent handler written in Go, with the rutis Go SDK.
//
// It registers a handler — a map of Go functions — with the `yoagent` host
// service: a policy (`before_tool`), a redaction (`after_tool`) and tools
// (`tools`, `call_tool`). Each hook gets one argument, an object decoded
// into a map; its `signal` field decodes as nil: the host's cancel arrives
// as the hook's context.Context instead, cancelled when the host gives the
// call up (the run cancelled, a hook timed out, the plugin unloaded). As Go
// contexts do, it also ends when the hook returns: watch it only while the
// call runs.
// Returning an error fails closed, as a throw does in TypeScript: a policy
// denies, a redaction withholds, a tool call fails.
//
// tests/go_test.rs builds and runs it; `config.out` is a file it writes
// what happened to.
package main

import (
"context"
"errors"
"fmt"
"os"
"strings"
"time"

"github.com/arcships/rutis/go/rutis"
)

type Config struct {
Out string `json:"out"`
}

// record appends `line` to the file `path`; a failure goes to stderr, which
// reaches the host's.
func record(path, line string) {
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err == nil {
_, err = fmt.Fprintln(f, line)
if closeErr := f.Close(); err == nil {
err = closeErr
}
}
if err != nil {
fmt.Fprintf(os.Stderr, "go-policy: record %q to %s: %v\n", line, path, err)
}
}

var Plugin = rutis.Define(rutis.Plugin[Config]{
Name: "go-policy",
Inject: []string{"yoagent"},
Apply: func(ctx *rutis.Ctx, config Config) error {
var yoagent rutis.Service
if err := ctx.Use("yoagent", &yoagent); err != nil {
return err
}
handler := map[string]any{
"before_tool": func(call context.Context, c map[string]any) (any, error) {
tool, _ := c["tool"].(string)
record(config.Out, "go before_tool: "+tool)
switch tool {
case "bash":
return map[string]any{"deny": "no shell from Go"}, nil
case "fail_gate":
return nil, errors.New("go policy backend down")
case "odd_gate":
return map[string]any{"allow": "maybe"}, nil
case "slow_gate":
select {
case <-call.Done():
record(config.Out, "go slow_gate: cancelled")
return nil, call.Err()
case <-time.After(60 * time.Second):
return nil, nil
}
}
return nil, nil
},
"after_tool": func(_ context.Context, c map[string]any, output map[string]any) (any, error) {
if c["tool"] == "go_echo" {
text, _ := output["text"].(string)
return map[string]any{"text": strings.ReplaceAll(text, "secret", "[go redacted]")}, nil
}
return nil, nil
},
"tools": func(context.Context, map[string]any) (any, error) {
return []map[string]any{
{"name": "go_echo", "description": "echoes its arguments from Go"},
{"name": "go_wait", "description": "waits until cancelled"},
}, nil
},
"call_tool": func(call context.Context, c map[string]any) (any, error) {
switch c["tool"] {
case "go_wait":
record(config.Out, "go wait: started")
<-call.Done()
record(config.Out, "go wait: cancelled")
return nil, call.Err()
}
return fmt.Sprintf("go says %v", c["args"]), nil
},
}
var dispose *rutis.Function
if err := yoagent.Call(ctx, "register", &dispose, "go-policy", handler); err != nil {
return err
}
if dispose == nil {
return errors.New("register returned no disposer")
}
record(config.Out, "go registered")
// The handler goes when the plugin unloads.
ctx.Effect(func(c context.Context) error { return dispose.Call(c, nil) })
return nil
},
})

func main() { rutis.Serve(Plugin) }
1 change: 1 addition & 0 deletions integrations/yoagent-rutis/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ impl RutisBridge {
feature = "node",
feature = "python",
feature = "bun",
feature = "go",
feature = "websocket"
))]
crate::languages::provide(ctx, &registry)?;
Expand Down
6 changes: 3 additions & 3 deletions integrations/yoagent-rutis/src/languages.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Plugins in other languages — TypeScript / JavaScript on Node or Bun, and
//! Python — through [rutis-bridge](https://docs.rs/rutis-bridge) 0.9 (the
//! `node`, `bun`, `python` and `websocket` features).
//! Plugins in other languages — TypeScript / JavaScript on Node or Bun,
//! Python and Go — through [rutis-bridge](https://docs.rs/rutis-bridge) 0.9
//! (the `node`, `bun`, `python`, `go` and `websocket` features).
//!
//! With one of those features, [`RutisBridge::install`](crate::RutisBridge::install)
//! also provides the registry to language plugins as the host service
Expand Down
8 changes: 5 additions & 3 deletions integrations/yoagent-rutis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
//! yoagent itself knows nothing about rutis; the bridge uses only yoagent's
//! public API.
//!
//! TypeScript and Python handlers (features `node`, `bun`, `python`, `websocket`)
//! TypeScript, Python and Go handlers (features `node`, `bun`, `python`, `go`, `websocket`)
//! can return images in tool results and write logs into the host's
//! `tracing` output (target `yoagent_rutis::plugin`). Plugins from other
//! agent ecosystems — DSH tool plugins, pi extensions — plug in through
Expand Down Expand Up @@ -87,10 +87,10 @@
//!
//! # Plugins in other languages
//!
//! With the `node`, `bun`, `python` or `websocket` feature (rutis-bridge 0.9),
//! With the `node`, `bun`, `python`, `go` or `websocket` feature (rutis-bridge 0.9),
//! [`RutisBridge::install`] also provides the registry to plugins in other
//! languages, as the host service `yoagent`. Such a plugin registers an
//! object (or a Python dict) of async functions, with the same hooks and the
//! object (a Python dict, a Go map of functions) of async functions, with the same hooks and the
//! same plain-JSON arguments as a Rust [`Handler`]:
//!
//! ```ts
Expand Down Expand Up @@ -151,6 +151,7 @@ mod host;
feature = "node",
feature = "python",
feature = "bun",
feature = "go",
feature = "websocket"
))]
#[cfg_attr(
Expand All @@ -159,6 +160,7 @@ mod host;
feature = "node",
feature = "python",
feature = "bun",
feature = "go",
feature = "websocket"
)))
)]
Expand Down
Loading
Loading