Skip to content

feat(rutis): pi extensions adapter - #265

Merged
yuanhao merged 20 commits into
mainfrom
feat/pi-extensions-adapter
Oct 8, 2026
Merged

yuanhao merged 20 commits into
mainfrom
feat/pi-extensions-adapter

Conversation

@yuanhao

@yuanhao yuanhao commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

pi coding-agent extensions' tools and tool policies, running unchanged in yoagent through the rutis bridge.

plugins/pi/pi-extensions-adapter.ts loads pi extensions with pi's own loader (pi 1.1.0 installed in plugins/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:

pi yoagent
registerTool a tool (pi's activation rules, first registration wins); prepareArguments + pi's validation before any policy; runs only with the arguments its policies judged
setActiveTools an enforced allowlist over the tools pi knows (MCP / sub-agent / host tools unaffected, as in pi)
tool_call before_tool for every call — yoagent's built-ins under pi's names (write_file → write, …), arguments translated both ways, relative paths resolved where the policy looks; block denies, terminate stops the run, in-place input edits rewrite (a field yoagent's tool lacks → deny); an override of a built-in is enforced (read_file denied when an extension provides read)
tool_result after_tool; a throwing handler withholds the result (pi skips it)
input on_input: handled / transform / throw reject
before_agent_start additions to the system prompt → a note on each request of the run

Fails 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 in allowUnmapped; a pi tool named like a yoagent built-in (pi's sandboxed bash) unless listed in withoutBuiltins. 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 with strict. 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's write_file / bash; claude-rules, pirate, dirty-repo-guard, confirm-destructive: load (their features need commands / session events / .claude/rules).
  • plan-mode: refused (its context handler); tools, preset: load (state via commands); sandbox: refused without --without bash, with it pi's bash runs (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. Example examples/pi_extensions.rs (any extension files; --live DeepSeek; --without), run scripted in CI. CI: npm ci in plugins/pi with 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.executeTool and session history are unavailable; terminate is 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

yuanhao and others added 6 commits October 8, 2026 10:48
…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
@yuanhao yuanhao changed the title feat(rutis): pi extensions adapter (spike) feat(rutis): pi extensions adapter Oct 8, 2026
…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
yuanhao and others added 11 commits October 8, 2026 21:19
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
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
…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
yuanhao marked this pull request as ready for review October 8, 2026 21:27
@yuanhao
yuanhao merged commit ed4b945 into main Oct 8, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant