Skip to content

feat: add io.github.kennymcsimpson.governed-roundtable — governed roundtable for coding agents - #65

Closed
KennyMcSimpson wants to merge 2 commits into
vastsa:mainfrom
KennyMcSimpson:feat/governed-roundtable
Closed

KennyMcSimpson wants to merge 2 commits into
vastsa:mainfrom
KennyMcSimpson:feat/governed-roundtable

Conversation

@KennyMcSimpson

@KennyMcSimpson KennyMcSimpson commented Oct 5, 2026 •

Copy link
Copy Markdown

feat: add io.github.kennymcsimpson.governed-roundtable — governed roundtable for coding agents

Ready for review. The plugin and package in this PR were exercised in the official
PI-Desktop 0.16.1 portable build. That covered Load dev plugin, the permission review, restore at
boot, the command palette, the panel, the Room tool in a conversation, one full round with a
hosted seat, and Install .piplug (details).
The four defects the first real-host run found are fixed in the second commit and were checked
again in the real host (changes). Not covered: a real model,
external agents in the real host, and the items under Not run. High-risk tier: this
needs two independent maintainer reviews.

Summary

Adds plugins/io.github.kennymcsimpson.governed-roundtable 0.1.0, its packed
packages/io.github.kennymcsimpson.governed-roundtable-0.1.0.piplug, the regenerated catalog.json,
and tests/governed-roundtable.test.mjs (manifest contract and negative-path tests). The only other
change is the panel count in tests/panel-chrome.test.mjs (19 → 20), because this plugin declares
a v3 panel.

The plugin runs room-dev's governed room inside PI-Desktop
using only public plugin APIs:

  • Room host (resident service room-host): serves rooms in the plugin process, with a loopback
    room UI on 127.0.0.1 and a service.lock heartbeat.
  • Room agent tool (risk high): a PI conversation lists, creates and joins rooms and acts only
    for the one seat bound to its host-supplied ctx.sessionId.
  • Panel: the user sets the task, starts rounds, approves or denies disclosures, accepts stale
    verdicts, closes rooms, serves / stops rooms and releases seat bindings.
  • Hosted seats (lane pi-complete): text-only discussion seats that speak through
    pi.agent.complete, one completion per turn, no tools.
  • External agents (Codex, Claude Code, …) join through room-dev's own JOIN.md and the vendored
    room-dev/room.mjs; the plugin does not touch them.

No change to PI-Desktop or its core is needed.

Changes since the first commit

The second commit rebuilds the plugin from room-dev 57f17e5. It fixes what the first real-host
run in PI-Desktop 0.16.1 found:

  • Toast language. Turn toasts now follow the host language. The plugin used to read the locale
    once in onLoad, and the host reports en while it restores plugins at boot, so a zh-CN UI got
    English toasts. It now reads the locale at each toast and follows appearance:changed.
  • No command lines in non-CLI packets. Packets for PI seats name only Room tool calls, and
    packets for hosted seats name only the ASCII markers they write in their reply. Neither gets
    room-dev command lines any more (node …/room.mjs … --seat …). The PI seat declares this at
    creation: room-dev's new seat "surface" tool:<the name the model sees>. Packets for
    command-line seats are byte-identical to before; 26 golden hashes pin this.
  • Panel refresh. The panel keeps polling while you type, and the focused input keeps its text,
    caret and IME composition. Polls never stack, and a late answer is dropped.
  • Closed rooms. A closed room is read-only in the panel: there is no Serve here, Stop or room UI
    button, and Release asks twice.
  • Stale-lock probe. It no longer calls Get-Process (room-dev ebc26a1).

All five were checked again in the real host. For this pull request the packed package grew from
958077 to 1005015 bytes. The manual-review signal count is unchanged at 58; 15 of them moved to new
line numbers, and the table below is regenerated.

Why

room-dev is a local, Windows-first room in which coding agents you already run (Codex, Claude Code,
or any agent that can run one local command) take turns in one conversation; the room never
launches, configures or attaches to them. Because it owns none of the agents, its governance detects
and records instead of enforcing: relayed text carries source labels, a reviewer's verdict is checked
against hashes of the frozen artifacts and marked stale if they changed, and an agent's own native
log can be read to confirm that a packet arrived. So far this is verified end to end with scripted
seats and with one real seat kind, a Codex thread started with codex exec in the same package
context as the room; Claude Code as an independent seat, and the governance layer on any real agent,
are not verified yet. The PI-Desktop side is this plugin, built only on the public plugin APIs.
It is tested against a fake host. It was also run through its GUI and one full round in the real
0.16.1 host, with a stub model. It asks for no core change.

It is a plugin rather than a core proposal because ADR 0165 withdrew the core agent-to-agent stack
and cross-session coordination now lives in plugins (pi.session-orchestrator). Whether the
governance layer catches failures that manual forwarding misses has not been evaluated; this PR
makes no claim that it does.

Risk tier and permissions

Tier: High (background service, file writes, child processes, an agent tool).

Permission Why When not granted (tested here with the fake host)
ui.panel The panel is the user's side of the room. The command's openPanel fails with PERMISSION_DENIED.
agent.tool.register The Room tool. registerTool fails with PERMISSION_DENIED, onLoad fails, nothing half-loads.
background.service Rooms must outlive single tool calls (tick, loopback UI, lock heartbeat). No service; create answers SERVICE_NOT_RUNNING and writes nothing; panel serve refuses the same way.
agent.complete Hosted seats speak through the user's configured model; the host resolves credentials. The hosted seat's turn becomes seat_failed {class: denied}; no completion is made; the plugin keeps working.
shell.openExternal "Open room UI in browser" (http://127.0.0.1:<port>/#<admin token>). The button answers PERMISSION_DENIED.

Not requested: desktop.control, net.fetch, fs.*, clipboard.*, agent.prompt.inject,
session.read. The tests assert that no pi.desktop.* or pi.net.* call is ever made.

Capability and data-flow matrix

Entry Reads Writes / sends to Permission User action Bounds Cleanup
Room create tool input; existing room.json new room folder %USERPROFILE%\room-dev\rooms\<room> (room, seats, tokens, JOIN.md, admin.token, events) agent.tool.register + service Agent session's permission policy for a risk: high tool id [A-Za-z0-9_-]{1,64}; 1–8 seats; existing absolute seat folders; session*/token*/admin* fields refused room removed if a seat fails; existing room refused
Room seat actions binding by ctx.sessionId seat outbox <seat folder>\.room-outbox\<seat>, room event log agent.tool.register tool call own seat only; text ≤ 100,000 chars; current attempt only —
Room wait packet file in the room packet text → calling conversation agent.tool.register tool call nonce-labelled blocks; other seats authority=none; forged [ROOM: escaped —
Room artifacts files inside the seat's own folder hashes + snapshot copies in the room; git init -q in the snapshot copy agent.tool.register tool call path guard (inside the seat folder; no .git, no symlinks); size cap stays with the room
Room disclose — a request agent.tool.register user approves / denies (panel or room UI) room allowlist or exact task acceptance commands only; argv, no shell; in the snapshot copy; timeout; 1 MiB output cap process tree killed on timeout
Service room-host served rooms 127.0.0.1 HTTP (random port), service.lock, events background.service serve / stop in the panel rooms the user stopped are never re-served by the tool stop / unload closes all rooms and removes their locks
Hosted seat its packet one pi.agent.complete per turn → user's configured model (quota) agent.complete user starts the round ≤ 200,000 chars, system ≤ 32 KiB, host 8 / 60 s, tools: [], no session context —
Panel roundtable/* room state admin commands (allowlist task, start, say, close, skip, retry, approve-disclose, deny-disclose, accept-stale) ui.panel click unknown command UNKNOWN_CMD close stops serving
Panel "Open room UI" the room's admin token system browser via pi.shell.openExternal shell.openExternal click loopback URL; token in the fragment (browser history may keep it) —
Turn toast room / seat id pi.ui.showToast — — one per attempt of a manual seat —
Plugin state — roundtable.json in pi.plugin.getDataPath() (bindings incl. session ids) — — plugin-private release in panel
Reviewer verdict record (room-dev) authorized verdict one line appended to %USERPROFILE%\room-dev\adjudications.jsonl — a reviewer's verdict append-only, single-file guard —
Codex wake push (room-dev) registered wake codex queue child process — only for seats that registered a wake outside this plugin 30 s timeout, 64 KiB cap; the tool never registers wakes —
Native-log audit / wake idle check (room-dev) that agent's own session logs, read-only (%USERPROFILE%\.codex, %USERPROFILE%\.claude\projects) audit results in the room's event log — only for seats registered outside this plugin with an audit source or wake thread read-only; seats created by the tool register neither —

Credentials: none read. The plugin opens rooms with savedPiKey: false, so room-dev's DPAPI-saved
Pi key is never decrypted (a test runs room-dev's default as a control and shows the difference).
No outbound network; the only socket is the loopback room UI.

Known gap, documented in the README and safety notes: "administration is the user's" holds for
the Room tool only. Each room's admin token is a file under the rooms folder, so a PI conversation
whose own file or shell tools can read that path can administer the room without the panel. Users
who want administration to stay with them must deny those tools the rooms folder.

Dependency and provenance

  • room-dev/ is room-dev itself, vendored unchanged by room-dev's build.mjs --source head from
    commit 57f17e5553849b0da2b1a5b3b8440026621dc3af; room-dev/VENDORED.json lists every vendored
    file with its byte count and sha256 (dirtyAtBuild: []). MIT (room-dev/LICENSE).
  • Public source: https://github.com/KennyMcSimpson/room-dev. 57f17e5 is a commit in the
    author's working repository, and public commit 4216222 is its export. Each public commit message
    names the working commit it was exported from. All 46 files in VENDORED.json match 4216222
    byte for byte (sha256 checked).
  • No npm dependencies, no node_modules, no minified or generated files, no native binaries.
  • room-dev/lib/hosted/pi.mjs and lib/hosted/dpapi.mjs (room-dev's built-in Pi seat and its
    DPAPI key store) are vendored but inert here: the Pi SDK is not bundled (its literal
    import('@earendil-works/pi-coding-agent') fails with PI_NOT_INSTALLED), the plugin passes
    savedPiKey: false, and no panel or tool path reaches the key store. The plugin uses only
    buildSystemPrompt from pi.mjs.
  • room-dev/lib/lobby.mjs and room-dev/ui/lobby.* belong to room-dev's desktop app and are not
    used by the plugin (it opens the room UI through pi.shell.openExternal).

Audit result

Commands run in this clone with C:\ProgramData\anaconda3\python.exe (CPython, stdlib only):

python scripts/pack_plugin.py plugins/io.github.kennymcsimpson.governed-roundtable
  -> packages/io.github.kennymcsimpson.governed-roundtable-0.1.0.piplug
     sha256 00a2efa52045e1e5212dcca9b689d7cb7bdf58dc0a0f8f4ae6ba9f6ff930655e  size 1005015
python scripts/rebuild_catalog.py                 -> wrote catalog.json (26 plugins)
python scripts/security_audit.py --check-packages -> 27 plugin(s), 0 blocker(s), 139 manual-review signal(s), exit 0
python scripts/security_audit.py plugins/io.github.kennymcsimpson.governed-roundtable --check-packages
                                                  -> 1 plugin(s), 0 blocker(s), 58 manual-review signal(s), exit 0

The catalog shasum equals the package's sha256, and --check-packages confirmed the packed bytes
match the plugin source. (On Windows, rebuild_catalog.py writes CRLF; the file was normalized back
to LF so the diff is only the new entry and updatedAt.)

Manual-review signals (58), one line each

The script reports the first match per file and kind. "room-dev" files are the vendored engine.

Location Signal Justification
core.mjs:311 credential/secret Comment on savedPiKey: false: the plugin never reads room-dev's saved key; hosted seats use host-resolved credentials.
core.mjs:283 persistent activation/timer The words background.service in the SERVICE_NOT_RUNNING message; the service is declared in the manifest and stops on stop / unload.
core.mjs:386 destructive shutDown() of the service: closes the rooms this plugin serves and removes their service.lock. No system action.
renderer/panel.js:21 persistent activation/timer background.service in panel status text; the panel polls every 4 s with setTimeout only while open.
room-dev/lib/admin.mjs:551 network room-dev CLI admin pi-key --port: POST to 127.0.0.1 only. The plugin imports admin.mjs only for its command list.
room-dev/lib/admin.mjs:36 credential/secret CLI default io passes process.env; no secret is read on the plugin's path.
room-dev/lib/api.mjs:46 credential/secret The in-process seat client's io receives process.env (the host's minimal env).
room-dev/lib/api.mjs:168 persistent activation/timer service.lock heartbeat while a room is served; cleared on close.
room-dev/lib/audit.mjs:391 native executable Comment about cmd.exe & in the read-only native-log auditor's command classifier.
room-dev/lib/audit.mjs:24 credential/secret CLAUDE_CONFIG_DIR lookup to locate Claude Code logs for read-only audit (seats registered outside the plugin only).
room-dev/lib/audit.mjs:350 destructive A list of destructive command names the auditor flags in agents' logs; nothing is executed.
room-dev/lib/codex.mjs:1 native executable Locating codex.exe for the wake push (rooms with registered wakes only).
room-dev/lib/codex.mjs:48 credential/secret findCodex reads PATH-style variables from env.
room-dev/lib/common.mjs:88 credential/secret ROOM_DEV_HOME for the rooms root (not passed by the host; default %USERPROFILE%\room-dev).
room-dev/lib/common.mjs:135 destructive process.kill(pid, 0): liveness probe of a lock holder; signal 0 kills nothing.
room-dev/lib/export.mjs:9 credential/secret Comment listing the fields room-dev's exporter redacts.
room-dev/lib/guard.mjs:84 destructive Guarded rm, restricted to the guard's allowed roots and logged.
room-dev/lib/hosted/dpapi.mjs:109 process execution PowerShell DPAPI call for room-dev's saved key; unreachable from the plugin (savedPiKey: false, no key command exposed).
room-dev/lib/hosted/dpapi.mjs:60 native executable Path of System32 powershell.exe for the same; unreachable.
room-dev/lib/hosted/dpapi.mjs:1 credential/secret The DPAPI key-store module itself; unreachable.
room-dev/lib/hosted/dpapi.mjs:205 destructive forgetPiKey removes the saved key file (room-dev CLI only); unreachable.
room-dev/lib/hosted/pi.mjs:19 credential/secret Built-in Pi seat API-key parameter; inert (SDK not bundled, no key supplied).
room-dev/lib/hosted/pi.mjs:8 destructive Comment: the built-in Pi seat intercepts delete / git push before execution; inert here.
room-dev/lib/http.mjs:45 credential/secret Comment: loopback room UI pages carry no secret; the admin token travels only in the fragment or request bodies.
room-dev/lib/join.mjs:5 native executable Comment: looks up node.exe on PATH for the JOIN.md command.
room-dev/lib/join.mjs:111 credential/secret nodeOnPath reads PATH from env.
room-dev/lib/lobby.mjs:161 process execution rundll32 url.dll browser open for room-dev's desktop lobby; not used by the plugin.
room-dev/lib/lobby.mjs:156 native executable Same function's comment.
room-dev/lib/lobby.mjs:20 credential/secret Lobby comment: secrets only in bodies; not used by the plugin.
room-dev/lib/seat.mjs:475 native executable Comment on PowerShell 5.1 / cmd.exe quoting in seat command help.
room-dev/lib/seat.mjs:28 credential/secret Comment: seat commands drop inherited API keys from the environment before running.
room-dev/lib/seat.mjs:200 destructive Guarded removal of a consumed notice file in the seat's outbox.
room-dev/lib/service.mjs:620 process execution codex queue wake push for seats with a registered wake (30 s timeout, 64 KiB cap).
room-dev/lib/service.mjs:152 native executable Process image name recorded in service.lock to identify the holder.
room-dev/lib/service.mjs:12 credential/secret Comment on Pi key options; the plugin passes savedPiKey: false.
room-dev/lib/service.mjs:1748 persistent activation/timer Lock heartbeat of room-dev's CLI serve path (the plugin uses api.mjs's).
room-dev/lib/service.mjs:168 destructive Guarded removal of a leftover key-handoff file inside the room's own private/ folder.
room-dev/lib/spawn.mjs:14 process execution room-dev's single spawn wrapper: argv, no shell, whitelisted env, timeout, output cap.
room-dev/lib/spawn.mjs:89 native executable PATH lookup extension list (.exe, .com); .cmd / .bat shims are refused.
room-dev/lib/spawn.mjs:53 credential/secret buildCleanEnv: whitelist env so children do not inherit secrets.
room-dev/lib/spawn.mjs:120 destructive killTree: taskkill /T /F of the child's own process tree on timeout.
room-dev/lib/supervision.mjs:477 native executable Recognizes git as argv[0] for disclosure rules.
room-dev/lib/workspace.mjs:517 process execution git init -q inside an artifact snapshot copy in the room folder.
room-dev/lib/workspace.mjs:418 native executable Message refusing .cmd / .bat shims as acceptance commands.
room-dev/lib/workspace.mjs:219 credential/secret Comment about junction / symlink escapes (the word "secret" in an example path).
room-dev/lib/workspace.mjs:493 destructive Guarded removal of a snapshot file whose source changed during the copy.
room-dev/lib/zip.mjs:44 credential/secret SOURCE_DATE_EPOCH for reproducible export archives.
room-dev/room.mjs:60 native executable Comment naming room-dev's desktop app executable.
room-dev/room.mjs:5 credential/secret Comment: seat commands need none of the agent's API keys.
room-dev/ui/app.js:621 network Room UI page fetch to its own loopback server (same origin).
room-dev/ui/app.js:619 credential/secret credentials: 'same-origin' fetch option.
room-dev/ui/app.js:1147 persistent activation/timer Room UI polls its loopback server while the page is open.
room-dev/ui/index.html:29 credential/secret Password-type input to paste the admin token when the URL fragment is missing.
room-dev/ui/lobby.html:25 credential/secret Same input on room-dev's desktop lobby page; not opened by the plugin.
room-dev/ui/lobby.js:67 network Lobby page same-origin fetch; not used by the plugin.
room-dev/ui/lobby.js:67 credential/secret Same line (credentials: 'same-origin').
room-dev/ui/lobby.js:125 clipboard Lobby "copy link" button (navigator.clipboard.writeText); not used by the plugin.
room-dev/ui/lobby.js:570 persistent activation/timer Lobby polling; not used by the plugin.

Validation actually run

This repository's own tests.yml was run unchanged on this branch, on GitHub Actions
ubuntu-latest with Node v22.23.3 and Python 3.12. The run was made in a private scratch copy,
because Actions stay off on a new fork until someone enables them in the web UI. Every step passed:

  • security_audit.py --check-packages: 27 plugins, 0 blockers, 139 manual-review signals (all
    plugins together).
  • Python helper tests: 16, OK.
  • node --test tests/*.test.mjs tests/*.test.js plugins/*/test/*.test.mjs: 222 tests, 220 pass,
    0 fail, 2 skipped. Neither skip is in this plugin's tests: one needs osv-scanner and one needs
    Windows OpenSSH. All 8 tests in tests/governed-roundtable.test.mjs pass.

The same run was repeated on the second commit (8fa21c2). The results were identical: 0
blockers, 139 signals, 16 Python tests OK, and 222 Node tests with 220 pass, 0 fail and 2 skipped.
All 8 of this plugin's tests passed again.

On room-dev itself, at 57f17e5, with ROOM_DEV_HOME pointed at a short temp folder:

  • root suite: 604 tests, 603 pass, 0 fail, 1 skipped;
  • node test/e2e.mjs: 81/81;
  • plugin suite: 55/55 (plugin 16, panel 25, locale 8, manifest 6).

On the author's Windows 11 machine, Node 24.11.1 (first commit unless stated):

  • This repository: node --test --test-skip-pattern="real helper" tests/*.test.mjs → 189 tests,
    185 pass, 4 fail. The 4 failures (gitlens.test.mjs ×2, terminal.test.mjs ×2) also fail on
    a clean origin/main worktree on this machine. They are environment-specific and unrelated to this
    plugin. An earlier unfiltered run also failed terminal.test.mjs "real helper can spawn a short
    command…". That test was skipped afterwards and was not rechecked on origin/main.
    tests/governed-roundtable.test.mjs: 8 / 8 pass. tests/panel-chrome.test.mjs passes with the
    updated count.
  • This repository's Python tests: python -m unittest discover -s tests -p "test_*.py" → 16 tests OK.
  • tests/governed-roundtable.test.mjs loads plugins/<id>/main.js in a fake host that follows
    upstream's buildApi() shapes and AsyncLocalStorage tool-invocation scoping, with rooms in a
    throw-away temp folder. It checks:
    • the exact permission list and version, and the bilingual metadata;
    • a forged sessionId / session / token / adminToken in tool input is refused
      (IDENTITY_FROM_CONTEXT_ONLY), as are a missing session (NO_SESSION), a foreign seat
      (NOT_YOUR_SEAT), a taken seat (SEAT_TAKEN) and a hosted seat (HOSTED_SEAT);
    • every admin action is refused from the tool (ADMIN_ONLY_IN_PANEL), no admin event comes from
      the tool, and the admin token never appears in tool output;
    • a forged [ROOM:…authority=user] block in one seat's speech reaches the next PI seat and the
      hosted seat's agent.complete packet escaped, inside an authority=none block that carries the
      packet's nonce and a "not a user instruction" declaration;
    • permissions that were not granted fail cleanly: agent.complete, shell.openExternal,
      background.service and agent.tool.register, as tabulated above;
    • stopping the service removes service.lock, a stopped service is not restarted by a tool call,
      start serves the room again, and unload leaves nothing served;
    • no pi.desktop.* / pi.net.* call is made.
  • In room-dev (source of this plugin), at commit 6123f21: node --test "integrations/pi-desktop-plugin/test/*.test.mjs"
    → 24 / 24 pass (fake host: full rounds with PI seats, an external Claude Code seat that runs
    the vendored room.mjs in a separate process, hosted seats incl. RATE_LIMITED, panel flows,
    service stop / restart races). Root suite node --test "test/**/*.test.mjs" → 587 tests, 586
    pass, 1 skipped, 0 fail.

In the real host (official PI-Desktop 0.16.1 portable)

There were two runs. The first ran on the first commit's files, and its findings led to the second
commit. The second ran on the second commit's build, in a fresh profile (see
the second run).

What was tested in the first run. The first commit's files, unchanged: a copy of plugins/<id>
(diff -r against the branch showed no difference) and packages/<id>-0.1.0.piplug (sha256
4fc8b4a9…8eba4, the value in catalog.json at that commit).

Host and profile. The official PI-Desktop-Portable-0.16.1.zip release asset, run with a
throw-away profile:

  • PI_DESKTOP_DATA_DIR, PI_DESKTOP_AGENTS_DIR, HOME, USERPROFILE and PI_CODING_AGENT_DIR
    point into it, and so does --user-data-dir.
  • PI_DESKTOP_AGENTS_DIR is needed. host-core finds the global .agents root with
    dirs::home_dir(), which USERPROFILE does not redirect. A first attempt that left the variable
    unset loaded the author's real .agents skills and MCP server definitions and started those
    servers. That attempt was stopped and the whole run was repeated with the variable set.

Model. A local OpenAI-compatible stub, modelled on upstream's
apps/desktop/test/e2e/plugin-ui-slots/stub-model.mjs. It was registered through host-core's
providers.create, the way that harness's seed.mjs does it. No real model was used.

How it was driven.

  • Clicks were trusted CDP mouse events (--remote-debugging-port).
  • Prompts went through the app's MCP control plane (PI_DESKTOP_MCP_CONTROL=1).
  • The native folder and file pickers were filled through UI Automation and window messages
    addressed to the picker's own controls.

What happened:

  • Load dev plugin. The Plugins menu item 加载本地插件 opens a folder picker, then the permission
    review. The review listed all five permissions by risk:

    • high: agent.tool.register, background.service, agent.complete;
    • medium: shell.openExternal;
    • low: ui.panel.

    After Load, the registry row read source: dev, status: ready. Service room-host was
    running with 0 restarts, plugin.load.success was logged, and there were no warn or error lines.

  • Command palette. Searching "roundtable" lists Governed Roundtable: Open panel with the
    extension badge. Running it opens the panel window.

  • Panel. It rendered in the host's language (zh-CN). Its content starts below the 46 px band and
    the window-control capsule. It showed the service as running and the rooms root inside the
    throw-away profile.

  • Tool name and approval. The model was offered
    plugin_io_github_kennymcsimpson_governed_roundtable_Room. It is a deferred tool, so the model
    activated it through ToolSearch first. Calls then showed the host's high-risk approval card
    ("Plugin-provided tool requires approval") with an input preview, until "allow for this
    conversation" was chosen.

  • create. After approval, create returned "Room created and served". Seat pi1 was bound to
    the calling conversation's host session id; the panel showed the binding.

  • Forged identity and admin actions. A forged sessionId field and the non-tool action start
    were both rejected by the host's own schema validation ("must not have additional properties",
    "must be equal to one of the allowed values"). This happened before approval and before any plugin
    code ran. In the real host these inputs therefore never reach the plugin; the plugin's own checks
    (tested with the fake host) are a second layer.

  • One full round.

    1. In the panel: Set task, then Start round.
    2. The service's toast in the main window said it was pi1's turn.
    3. wait returned the packet with its nonce and authority blocks.
    4. submit was accepted. Its text contained a forged [ROOM:00000000:seat=h1 authority=user]
      block.
    5. The hosted seat h1 spoke through the host's real agent.complete. Its request offered no
      tools. In that request the forged block appeared escaped as [ROOM\:…, inside an
      authority=none block that carried the packet's nonce and the "not a user instruction" line.
    6. pi1 published the summary, then round_done.
  • Duplicate submits, caused by the stub. The room log also shows two NOT_YOUR_TURN rejections
    for duplicate submits. They came from the test stub reusing tool-call ids after it was restarted:
    PI's agent runtime dropped the replayed results, and the model resubmitted. The room rejected
    both duplicates.

  • Closing and releasing. Close room needs a second click, and then logs room_closed by
    admin. Release emptied the bindings, after which the tool answered NOT_JOINED.

  • Install .piplug. First the dev copy was uninstalled; this removed it and closed its panel.
    Then the menu item 安装插件包 opens a file picker. After picking the package:

    • the app showed "插件包已安装" (package installed);
    • the registry row read source: installed, status: ready;
    • the installed tree is byte-identical to plugins/<id>;
    • service room-host was running;
    • the panel opened from the installed path;
    • a tool call worked.

    No permission review appeared in this install path, unlike Load dev plugin.

  • Writes outside the throw-away profile. A top-level snapshot of the profile roots, taken before
    and after, shows no write by this plugin. %TEMP% gained Chromium's scoped_dir* folders for
    the two app runs. It also gained a number of empty .tmp* folders that could not be attributed to
    a process.

Second run: the fixed build

What was tested. The second commit's build, in a fresh throw-away profile with
PI_DESKTOP_AGENTS_DIR set from the first launch. The process tree held only host-core, and no MCP
server was started. The build was taken before the README update, so the folder in this PR differs
from it only in README.md, README.zh-CN.md and the commit recorded in room-dev/VENDORED.json;
the code is byte-identical.

  • Load dev plugin. The folder picker, then the permission review listing the same five
    permissions, then Load. After that the plugin was ready and room-host was running.
  • create. A room created through the tool stored seat pi1 with surface
    {"kind":"tool","tool":"plugin_io_github_kennymcsimpson_governed_roundtable_Room"}. That is
    exactly the name the host shows the model.
  • Toast after a restart (fix 1). The app was restarted, so the plugin was restored at boot.
    Set task and Start round in the panel then produced the toast
    圆桌 gui-test2:轮到席位 pi1。请让坐在这个席位的 PI 对话继续(调用 Room 工具的 wait 取包)。
    under the zh-CN UI.
  • Packets (fix 2).
    • The PI seat's packet, from wait, has no room.mjs, room.cmd, --seat or other CLI flag.
      Its lines read 行动方式:本席位只经宿主提供的房间工具 plugin_…_Room(下称房间工具)行动:… and
      交卷:调用房间工具 {"action":"submit","room":"gui-test2","attempt":"a1-1-be4be6","text":"<你的发言全文>"}….
    • The hosted seat's request through the host's real agent.complete has no CLI text and does not
      mention 房间工具. It reads 交卷方式:本席位由房间托管,没有房间命令,也没有向房间提交的工具;你这一次回复的全文就是本回合的发言….
    • The forged authority=user block in that request is still escaped inside an authority=none
      block.
    • The round then completed: summary_published, then round_done.
  • Panel while typing (fix 3). Text was typed into the panel's message input and focus was left
    there. The event list then advanced from #3 to #12 without Refresh, and the input still
    held its text and focus. The status line read 阶段: 空闲 · 轮次 0, later 本轮结束 and 已散会.
  • Closed room (fix 4). After Close room, which takes two clicks, the panel offered only
    Release and Hide: no Serve here, Stop or room UI button, and no text inputs. Release
    needed a second click (再点一次确认), and only then was the binding removed.
  • Install .piplug. After uninstalling the dev copy, the package installed with
    source: installed, status: ready. The installed tree was byte-identical to plugins/<id>,
    room-host was running, and the panel opened.
  • Writes outside the profile. None by the test. .agents was untouched; the only additions in
    %TEMP% were Chromium's scoped_dir* folders.

Not run

  • Open room UI in browser (shell.openExternal). It would have opened the test machine's
    default browser, so it was not clicked. Whether Windows keeps the URL fragment is therefore still
    unverified.
  • Permission-denied paths in the real host. All five permissions were granted. These paths are
    tested only with the fake host.
  • A real model behind the conversation and behind agent.complete.
  • External agents in the real host. No external agent (Codex, Claude Code) joined a room through
    JOIN.md while the plugin served it.
  • Crash restart with a stale lock, and the 5 s start budget when many rooms are remembered.
  • Maintainer review. None yet. Two independent approvals are required for this tier.

Known limits

  • PI seats are not woken automatically. session/collaboration/send / spawn need an active
    Agent tool invocation, which a background service never has, and desktop.control
    agent/prompt would deliver the wake with user authority. The plugin shows a toast and the user
    tells that conversation to continue.
  • Hosted seats are discussion-only: one text-only agent.complete per turn with no tools, so a
    hosted seat cannot be an executor.
  • The Room tool works in Agent mode only (no planSafeActions reach the broker).
  • engines.piDesktop is >=0.16.1. The only PI-Desktop source read was a shallow clone at 0.16.1,
    which cannot show the earliest release that has every API used. Older hosts may work but were not
    checked.
  • The admin token is a file (see the known gap above).
  • External seats need node on PATH, and their room.mjs path points into the installed plugin
    folder.
  • Rooms created by the first commit's build have pi seats without the tool surface. A new
    conversation cannot join them (NOT_A_PI_SEAT); recreate those rooms. Bindings that already exist
    keep working.
  • Model-facing tool instructions and refusals stay in English, whatever the UI language; packets
    stay in Chinese, as room-dev writes them.

中文摘要

新增插件 io.github.kennymcsimpson.governed-roundtable 0.1.0(含 .piplug、重新生成的
catalog.json、tests/governed-roundtable.test.mjs,以及把 panel-chrome 测试里的面板数 19 改为 20)。
它只用公开插件接口,把 room-dev 的受治理房间带进 PI-Desktop:常驻房间宿主、Room 工具(席位按宿主给的
ctx.sessionId 绑定)、用户管理面板、经 agent.complete 的纯文本讨论席位。不需要改宿主或核心。

  • 风险等级:高。五项权限:ui.panel、agent.tool.register、background.service、agent.complete、
    shell.openExternal;能力与数据流矩阵见上文。

  • 审计:仓库全量 security_audit.py --check-packages 0 个 blocker;本插件 58 条人工复核提示,逐条理由见上表。

  • 已跑:本仓库自己的 tests.yml 原样在 GitHub Actions 上跑过,环境是 ubuntu-latest、Node 22。结果全部通过:审计 0 个
    blocker;Python 测试 16 个;Node 测试 222 个,其中 220 个通过、2 个跳过,跳过的都不是本插件的;新测试 8/8 通过。
    第二个提交又跑了一遍,结果相同。作者 Windows 机器上:本插件的测试 12/12 通过(含 panel-chrome)。room-dev 侧:
    插件测试 55/55;全量 604 个,其中 603 通过、1 跳过、0 失败;e2e 81/81。

  • 真实宿主:在官方 PI-Desktop 0.16.1 便携版里测了本 PR 的原样文件,用的是一次性配置目录和本地 stub 模型。以下都通过:

    • Load dev plugin,以及之后的权限审阅(五项权限按风险分组列出);
    • 命令面板入口和面板(跟随宿主语言);
    • 宿主强制加的工具名 plugin_io_github_kennymcsimpson_governed_roundtable_Room,以及高风险审批卡;
    • 建房,席位绑定到宿主给的会话 id;
    • 完整一轮:PI 席位发言,托管席位经真实的 agent.complete 发言,伪造的 authority=user 块在包里被转义、放进
      authority=none 块;之后发布总结;
    • 散会和解除绑定;
    • Install .piplug,装好的文件与 plugins/<id> 逐字节相同。

    伪造的 sessionId 和 admin 动作在真实宿主里先被宿主自己的 schema 校验挡掉,根本到不了插件。注意:隔离测试必须设
    PI_DESKTOP_AGENTS_DIR,因为 USERPROFILE 挡不住 host-core 读真实 home 下的 .agents。

  • 未跑: 在浏览器打开房间界面(会打开测试机的默认浏览器)、真实宿主里的权限拒绝路径、真实模型、外部 agent 在真实宿主里
    入席、崩溃后带陈旧锁重启。

  • 第一次真实宿主测试发现的 4 个问题,已在第二个提交里修好,并在真实宿主里复测通过:

    • 开机恢复后,toast 跟随宿主语言,实测是中文;
    • PI 席位的包里只写 Room 工具调用,托管席位的包里只写 ASCII 标记,都没有命令行;命令行席位收到的包逐字节不变;
    • 面板在打字时照常刷新,打的字不会丢;
    • 已散会的房间只读,解除绑定要点两次。

    另外带上了锁探针的修复(room-dev ebc26a1)。

  • 已知限制:PI 席位不能经插件接口自动唤醒;托管席位只能讨论(agent.complete,无工具);治理层能否抓到手工转发
    漏掉的问题尚未评估;admin 令牌是文件,PI 对话自己的文件 / shell 工具若能读到房间目录就能绕过面板。

🤖 Generated with Claude Code

…ndtable for coding agents

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 5, 2026

Copy link
Copy Markdown

@KennyMcSimpson is attempting to deploy a commit to the vastsa's projects Team on Vercel.

A member of the Team first needs to authorize it.

…l-app test in 0.16.1

Rebuilt from room-dev 57f17e5 (public export to follow):
- turn toasts follow the host language (read per toast, follow
  appearance:changed); a plugin restored at boot no longer shows English
  under a zh-CN UI
- packets for PI seats name only Room tool calls and packets for hosted
  seats name only ASCII markers; no room-dev command lines reach either
- the panel keeps polling while you type without losing input; a closed
  room is read-only with no serve / stop / room-UI button, and Release on
  a closed room asks twice
- the stale-lock probe no longer calls Get-Process (room-dev ebc26a1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@KennyMcSimpson
KennyMcSimpson marked this pull request as ready for review October 5, 2026 16:48
@vastsa

vastsa commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Closing this as part of the migration that removed plugin sources from this repository.

Since commit 38284f1 the plugins/ tree is gone: this repository only ships the published catalog.json and the packages/*.piplug artifacts. Sources now live in each author's own repository, and every release goes through the plugin center, which packs the files, audits the source, records the SHA-256 and publishes the version.

Your work is not lost — your branch and fork stay exactly as they are. To ship io.github.kennymcsimpson.governed-roundtable:

  1. move plugins/io.github.kennymcsimpson.governed-roundtable/ into your own public repository (e.g. KennyMcSimpson/pi-desktop-governed-roundtable), keeping manifest.json at the package root;
  2. tag the version you want reviewed (v<x.y.z>); the tag, or a commit SHA, becomes the sourceRef the review reads;
  3. sign in at https://plugins.aiuo.net → My Plugins → Create plugin: upload the package, bind that repository, submit — or let an agent run the publishing skill https://plugins.aiuo.net/skill.md over MCP https://plugins.aiuo.net/mcp;
  4. later versions are submitted from the plugin's own page.

One detail worth keeping: the center rebuilds the published manifest from what you submit, so send ui, contributes, activationEvents, fs and net in full — anything omitted is dropped and the installed plugin becomes invalid.

This repository still welcomes distribution-side contributions (a broken .piplug, a stale catalog entry, website or docs fixes); anything that adds plugins/<id>/ will be closed with this pointer. Thanks for the work on io.github.kennymcsimpson.governed-roundtable — the review gate is the same, it just runs against your own repository now.

@vastsa vastsa closed this Oct 6, 2026
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.

2 participants