Repository navigation
feat(rutis): pi extensions adapter - #265
Merged
Merged
Conversation
…agent plugins/pi/pi-extensions-adapter.ts loads pi coding-agent extensions unchanged with pi's own loader (pi 1.1.0 packages installed in plugins/pi/) and maps registerTool, tool_call, tool_result, before_agent_start and session_start/shutdown onto one yoagent handler; everything else is reported (strict: load failure). Fixture extension, tests/pi_test.rs, examples/pi_extensions.rs (any extension files; --live with DeepSeek); CI installs plugins/pi and runs the scripted example. Tried with 11 of pi's own examples, unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
…mon paths - translate arguments only for yoagent built-ins (a pi tool named edit/grep keeps its own), and enforce an extension's override of a built-in - prepareArguments + pi's validation before the tool_call policies - after_tool: real content (images kept), only the fields a handler set - before_agent_start: each failing handler skipped alone, cached per run - first registration wins, defaultActive respected (pi's activation) - providers/MCP servers reported; theme proxy; getSystemPrompt in the hook; search ignoreCase, find pattern - example: tools act in the temp project, --without NAME - tests: fixture-extra.ts, less-common-paths and strict tests Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
… search case default - prepareArguments runs on a copy and the rewrite check compares with the raw call (pi's own edit prepare mutates in place: was silently lost) - the same-name check runs at load after session_start (strict refuses the load, not the first run); names/ARGS are Maps - search: yoagent's default case-insensitivity is ignoreCase: true - before_agent_start: a message is dropped, its handler's addition kept - call_tool no longer re-validates (execute gets what the policies left) - native providers and virtual models reported; shutdown effect registered before the strict checks - tests: prepare-only call, details landed, a good prompt handler after failing ones, strict same-name refusal Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
Critical (fail closed by default): - setActiveTools is an enforced allowlist (offered tools and calls, the built-ins under pi's names); other runtime actions throw - a throwing session_start handler, an unfired deciding event (context, message_end, before_provider_*; unless allowUnmapped) and a pi tool named like a yoagent built-in (unless withoutBuiltins) refuse the load; found after load they stop the adapter (deny/reject/stop) - a throwing tool_result handler withholds the result Important: - call_tool runs a pi tool only with the arguments its policies judged - terminate: true stops the run at its next model request - a relative path is resolved against cwd before the policies judge it - a rewrite to a field the yoagent tool lacks is denied - input → on_input (handled/transform/throw reject) - example self-check asserts the denial; shutdown errors propagate - plugins/pi/node_modules excluded from the crate Also: onTerminalInput unsubscribe, 'then' on UI proxies, non-string systemPrompt, key-sorted JSON compares, docs corrected and dated. Tests: 8 (load refusals, same-name + withoutBuiltins, setActiveTools, per-run notes + input, judged arguments, and the less common paths: validation deny, find, built-in redaction, withheld result, unknown field, relative path, terminate, case-insensitive search, multi-edit deny); 12 safeguards mutation-checked. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
…y path resolution - the setActiveTools allowlist covers only tools pi knows; MCP, sub-agent and host tools are unaffected (as in pi) - as pi's _applyToolLoadout: a listed tool is available unless hidden (defaultActive ignored); one registered later that pi would activate joins the allowlist - relative paths are resolved only for yoagent's built-ins (an MCP tool's path may be a repository's) - getAllTools reports pi-shaped SourceInfo, skips withoutBuiltins and names an extension tool takes; tools() offers nothing once refused - canon compares non-integers at 15 significant digits (serde_json float round trip) - terminate documented as stricter than pi; stray rel.txt removed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
…etActiveTools pi refreshes its registry on registerTool, so register + setActiveTools( getActiveTools().filter(...)) keeps the new tool; getActiveTools now refreshes the allowlist first (and omits withoutBuiltins without one). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
setup-node's npm cache, keyed on the plugins/, plugins/dsh/ and plugins/pi/ lockfiles; pi's install alone is ~185 MB. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
…tis-agent
- bridge: call_tool results and after_tool edits take yoagent-shaped
content blocks (text, base64 images) instead of text; validated by
content_blocks
- dsh adapter: dsh image blocks (attachment references) become yoagent
images via the attachments service when one is loaded, else a text
placeholder
- rutis-agent example: a runner's {"content": [...]} value is read as
blocks (dot_picture)
- yoagent.d.ts ContentBlock; README, CLAUDE.md, CHANGELOG
- tests: content_blocks unit, TS/Python round trip, dsh with and without
a store; dsh_test no longer reads a mid-write empty abort file
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
# Conflicts: # CHANGELOG.md # CLAUDE.md
pi's text and image blocks are yoagent's shape: call_tool returns them as content blocks, and a tool_result content edit is sent back as blocks, so pictures survive. Fixture pi_picture + caption edit; test. pi_test no longer reads the slow tool's file mid-write. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
…limits - content_blocks decodes base64 (STANDARD), takes png/jpeg/gif/webp only, and refuses images over 10 MB (websocket frame limit): an image a provider refuses would sit in history and fail every later request - dsh adapter: dsh's offloaded images and images over 3.75 MB (under Anthropic's 5 MB base64) stay text; an error result's images are not read - rutis-agent example: blocks only with at least one image - docs: limits, picking fields in after_tool edits, text join Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
# Conflicts: # CLAUDE.md
An image identical to one in the output the edit was given passes as it is: yoagent's read_file takes bmp up to 20 MB, MCP tools other types, and keeping them must not withhold the result. New or changed images are checked as before. Tests: kept bmp passes, changed data does not; the size pre-check's own rejection. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
# Conflicts: # CLAUDE.md
…apter # Conflicts: # CHANGELOG.md # CLAUDE.md
… pi-latest CI - bridge: the yoagent service gains log(level, message) → tracing target yoagent_rutis::plugin (8 KiB cap); the pi and dsh adapters report through it (else stderr). Test: tests/plugin_log_test.rs (own binary) - pi adapter: ctx.executeTool runs pi tools as nested calls (prepare, validate, tool_call with parentToolCallId, tool_result; never rejects); ctx.tools lists them; yoagent's tools are not reachable - pi adapter: a deciding event registered mid-run is caught at the next model request or tool call, and stops that run - CI: weekly pi-latest workflow runs pi_test against the newest pi Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
…ool_result, log fallback
- pi-latest workflow: print versions from the installed packages (npm ls
exits 1 on a version off the exact pin, which is the case under test)
- executeTool: a nested tool's throw is an error result that still goes
through the tool_result handlers (as pi); missing args are {}; refused
once the adapter stopped; checks re-run per nested call; error details {}
- log: rutis gives a missing method a throwing stand-in, so adapters wrap
the call (try + .catch) and fall back to stderr
- README points to the UI-as-plugin-services design (yoyo-meme/yo#3 §7)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
pi app extensions record or show a tool's result through pi's session API inside the tool (pi-video-gen's appendEntry, pi-cavallo's sendMessage), after the paid work: refusing those calls failed the tool. They now go to the adapter's in-memory session (readable through ctx.sessionManager; a displayed message is also logged; a message never starts a turn). sendUserMessage and the rest still throw. Test: pi_render fixture tool. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
Review: sendMessage wrote a 'message' entry; pi's runtime writes a custom_message entry (appendCustomMessageEntry), which extensions filter on when they read their messages back. Content defaults to [], display as given (pi's semantics), odd content cannot fail the tool. The test now reads back both the custom entry and the custom_message. README notes the in-memory session grows until the plugin reloads. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
yuanhao
added a commit
that referenced
this pull request
Oct 8, 2026
Review of #268: - log never returns an error: a rejected async call a plugin does not catch ends its Node runtime (no unhandledRejection handler). Any message (non-strings as JSON), any context (other fields ignored, e.g. a whole hook argument). Test: a malformed uncaught call, then 'still alive'. - Python: log is a coroutine — documented as await yoagent.log(...) - docs: two ecosystems plug in today, pi is in review (#265); the adapter rules are a contract ('should'), with which rules the DSH adapter meets Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
yuanhao
added a commit
that referenced
this pull request
Oct 8, 2026
…e ecosystems (#268) * feat(rutis): plugin logs to the host, the adapter contract, one contract three ecosystems - bridge: the yoagent service gains log(level, message, {run_id}?) → tracing target yoagent_rutis::plugin with a run_id field; the dsh adapter uses it. Test: tests/plugin_log_test.rs (own binary) - docs: the extensions guide shows DSH, rutis-agent and pi plugging in through Extension + the bridge with no core change, and what the bridge gives every ecosystem (images, logs, fail-closed); the bridge README states the adapter contract (seven fail-closed rules) - CI: the rutis-bridge job runs the rutis-agent example - guide: yoagent-rutis is on crates.io Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG * fix(rutis): log never rejects; docs say what is in review Review of #268: - log never returns an error: a rejected async call a plugin does not catch ends its Node runtime (no unhandledRejection handler). Any message (non-strings as JSON), any context (other fields ignored, e.g. a whole hook argument). Test: a malformed uncaught call, then 'still alive'. - Python: log is a coroutine — documented as await yoagent.log(...) - docs: two ecosystems plug in today, pi is in review (#265); the adapter rules are a contract ('should'), with which rules the DSH adapter meets Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…experimental Bridge files from main (log never rejects, run_id context); the pi branch's log test kept as pi_log_test (the adapter's warnings reach the host). The pi and DSH adapters are marked experimental, expected to move to the yo app once it hosts plugins; the guide counts pi among the three. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
yuanhao
marked this pull request as ready for review
October 8, 2026 21:27
This was referenced Oct 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
pi coding-agent extensions' tools and tool policies, running unchanged in yoagent through the rutis bridge.
plugins/pi/pi-extensions-adapter.tsloads pi extensions with pi's own loader (pi 1.1.0 installed inplugins/pi/, ~185 MB, pinned exactly) and maps the agent-loop part of pi's API onto one yoagent handler, following pi 1.1.0's runner and agent loop:registerToolprepareArguments+ pi's validation before any policy; runs only with the arguments its policies judgedsetActiveToolstool_callbefore_toolfor every call — yoagent's built-ins under pi's names (write_file→write, …), arguments translated both ways, relative paths resolved where the policy looks;blockdenies,terminatestops the run, in-place input edits rewrite (a field yoagent's tool lacks → deny); an override of a built-in is enforced (read_filedenied when an extension providesread)tool_resultafter_tool; a throwing handler withholds the result (pi skips it)inputon_input: handled / transform / throw rejectbefore_agent_startFails closed by default — the load is refused on: an extension that fails to load; a throwing
session_start; a handler for an event yoagent never fires that would decide or rewrite something (context,message_end,before_provider_*) unless listed inallowUnmapped; a pi tool named like a yoagent built-in (pi's sandboxedbash) unless listed inwithoutBuiltins. Found after load, these stop the adapter (calls denied, prompts rejected). Everything else unsupported (observer and session events, commands, UI, renderers, providers, MCP servers) is reported, or refused withstrict. No UI: pi's print-mode answers (confirm→ false).Config:
extensions,cwd,name,toolNames,withoutBuiltins,allowUnmapped,strict. Hosts should install the bridge's extension with.require_policy()(and.rechecks_modified_calls()when other handlers rewrite calls).Tried (pi 1.1.0, Oct 2026), unchanged, scripted and live with DeepSeek:
hello,todo,tool-override,truncated-tool,dynamic-tools: tools work;protected-paths,permission-gate: judge yoagent'swrite_file/bash;claude-rules,pirate,dirty-repo-guard,confirm-destructive: load (their features need commands / session events /.claude/rules).plan-mode: refused (itscontexthandler);tools,preset: load (state via commands);sandbox: refused without--without bash, with it pi'sbashruns (the OS sandbox itself needs ripgrep, not installed here).Tests:
tests/pi_test.rs, 8 tests (end to end, less common paths, strict, same-name refusal, load refusals,setActiveTools, per-run notes + input, judged arguments); 21 safeguards mutation-checked. Exampleexamples/pi_extensions.rs(any extension files;--liveDeepSeek;--without), run scripted in CI. CI:npm ciinplugins/piwith an npm cache.Known limits: images from pi tools arrive as text (bridge result is text); warnings go to the Node process's stderr;
ctx.executeTooland session history are unavailable;terminateis stricter than pi; pi upgrades need a check (the loader is imported by file).Reviewed in four full rounds (code, tests, comments, silent failures) plus focused re-reviews of each fix round.
🤖 Generated with Claude Code
https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG