Skip to content

Modernize every bridge for PowerIO 0.11.2 - #64

Open
samtalki wants to merge 15 commits into
Power-Agent:mainfrom
samtalki:codex/powerio-011-modules
Open

samtalki wants to merge 15 commits into
Power-Agent:mainfrom
samtalki:codex/powerio-011-modules

Conversation

@samtalki

@samtalki samtalki commented Sep 5, 2026

Copy link
Copy Markdown
Member

PowerIO removed the 0.9 Package API in 0.11.0 and changed its default MCP path
policy in 0.11.2. This moves every PowerMCP bridge onto the typed PowerIO IR module
model, pins powerio[mcp,matrix]>=0.11.2,<0.12 in all fourteen places, and returns
the suite to green.

Solver boundary

powermcp/solver_case.py resolves one file or one serialized PowerIO IR document
into a typed module, selects a collection entry by time_index or scenario_id,
applies typed what-if edits through PowerIO's OperatingPointUpdate, NetworkUpdate
and apply_bus_load_active_power, and hands the solver a balanced network. It
re-parses and re-validates nothing PowerIO already states. Every adapter response
carries the same tail: value_type, selection, diagnostics, warnings, and the
emission fidelity where the adapter converts.

pandapower, PyPSA, ANDES and Egret take powerio_ir (with network_json as
an alias), edits, to_balanced, base_mva, time_index and scenario_id.
pandapower's hand-rolled PYPOWER-to-MATPOWER text projection is gone; PowerIO's
native writers do the conversion.

Path containment

powerio 0.11.2 confines MCP paths to the directory the server process started in when
no root variable names one, where earlier versions left every path allowed. The
doctor now distinguishes an explicit root list from that implicit default, and the
test suite names the repository and pytest's temporary tree as its roots. This is the
root cause of the 41 failures on this branch's previous head.

Also

  • surge and PyPSA create the staging directory that powerio 0.11.2 now hands to a
    directory writer unset.
  • ANDES and Egret install their emitted case file through powerio's staged file
    write, so a failed write leaves no truncated file behind.
  • Collection selection rebuilds the module after every level, and a wrong time_index
    or scenario_id is refused with the available entries listed.
  • Every adapter response, pandapower included, carries the warnings list.
  • Four print calls that wrote to the MCP stdio channel move to stderr and logging
    (ANDES, PowerWorld).
  • GenX stops leaking the repository root onto sys.path and declares its powerio
    dependency.
  • PLEXOSDB stops citing a server file this repository deleted, and joins the SDK
    import sweep.
  • CI drops the Rust toolchain step (powerio ships abi3 wheels) and cancels superseded
    pull request runs.
  • Version 0.4.0.

History

The change is the eight commits from 86dd105 to d1a8d91, rebuilt on current
main. The merge commit 064c0bc adopts that tree; the branch's earlier commits
stay reachable through its second parent and contribute nothing to the diff.
Review the eight commits or the combined diff against main.

Validation

  • pytest tests -q on Python 3.12 against powerio 0.11.2:
    358 passed, 19 skipped, 0 failed.
  • tests/test_server_entrypoints.py launches every registered server as a
    subprocess and lists its tools.
  • tests/test_transport.py drives the powerio server over real stdio through
    powermcp run powerio.
  • python -m build plus twine check on the sdist and wheel.

🤖 Generated with Claude Code

@samtalki samtalki changed the title Support PowerIO 0.11 modules in solver adapters Integrate PowerIO 0.11 modules and native Tellegen Studies Sep 5, 2026
@samtalki
samtalki marked this pull request as draft September 5, 2026 05:45
samtalki and others added 9 commits September 15, 2026 20:22
`resolve_solver_case` resolves one file or one serialized PowerIO IR document
into a typed module, selects a collection entry by `time_index` or
`scenario_id`, and hands a writer the balanced network an operating point
states. `OperatingPoint.network` supplies that network directly, so no
MATPOWER round trip narrows the state to a representable subset.

`pio-ir` and `pio` are the format tokens PowerIO accepts; a document carrying
neither is refused on PowerIO's own terms. Diagnostics follow PowerIO's
severity vocabulary: `error` fails validation, `remark` and `note` stay out of
the user-facing warning list.

`check_diagnostics`, `operating_point_module` and `select_entry` are public
because a second server module calls them. `value_type_name` isolates the one
structural-type read PowerIO publishes no accessor for.

`powermcp.sandbox` re-exports `staged_file_write` alongside the rest of the
path policy and states powerio's containment default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pandapower, PyPSA, ANDES, Egret and surge accept `powerio_ir` as the primary
argument, keep `network_json` as its alias, and splice the shared response
tail: `value_type`, `selection`, `diagnostics`, `warnings`, plus the emission
fidelity and the typed-edit report for the adapters that convert into their
own model.

PowerIO 0.11.2 hands a directory writer a path that does not exist yet, so
the PyPSA CSV export and the surge table export create the staging directory
before writing into it. ANDES and Egret install their one emitted case file
through `staged_file_write`, so a write that fails part way leaves whatever
stands at the destination untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fourteen declarations now read `powerio[mcp,matrix]>=0.11.2,<0.12`. The floor
is 0.11.2 because 0.11.1 added `OperatingPoint.network` and matrix `row_ids`,
and 0.11.2 sets the path-containment default this repository documents.
`GenX/requirements.txt` gains a pin: `GenX/server.py` imports
`powermcp.sandbox`, which imports powerio.

Version 0.4.0. The sdist excludes local virtual environments and uv build
caches, and `.gitignore` covers `.venv-*/`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PowerIO 0.11.2 confines MCP paths to the directory the process started in when
no root variable names one. A session-scoped autouse fixture names the
repository checkout, pytest's temporary base and the system temporary
directory, so a test writing under `tmp_path` is admitted. A test that sets its
own roots still overrides it, because `allowed_roots()` reads the environment
on every call.

`_containment_status` distinguishes an explicit root list from the implicit
default, reports the default as a setting to make rather than a policy already
chosen, and reports the refusal PowerIO raises for a root variable naming no
directory. The doctor's user-visible strings carry no decorative characters.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The powerio and solver-boundary tests require powerio 0.11.2, so the matrix
tools name their axes unconditionally and a retired Package document is
asserted for what it is: not PowerIO IR.

`tests/test_transport.py` states why it exists. The SDK parses a string
argument whose text is JSON before validation, so only a real stdio session
proves a `powerio_ir` argument reaches the tool intact.

`tests/test_sdk_imports.py` covers PLEXOSDB, the one server directory it
missed. `tests/test_publish_workflow.py` asserts the sdist exclude list keeps
local virtual environments and build caches out of a release.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The PowerIO section describes the server shipped in 0.11.2, names the
arguments every adapter takes, and states the containment default: with no
root variable set, paths stay beneath the directory the server process
started in.

Per-server READMEs describe what the import tools accept, which is serialized
PowerIO IR or one selected entry of a collection. The retired `Package`,
`model-json`, `package_json` and package `study_commit` formats keep their
migration note: re-parse the original case and pass its `powerio_ir`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
stdout carries JSON-RPC once a server runs, so the ANDES startup notes go to
its logger, which writes to stderr, and the two PowerWorld error reports go to
stderr explicitly.

`GenX/server.py` keeps the repository root on `sys.path` for the process
lifetime and says why: GenX carries no `__init__.py`, so its namespace
`__path__` is recomputed from `sys.path` on every attribute lookup and the
lazy `GenX.tool_logic` imports would fail without it.

PLEXOSDB's notes on its shared import name say what the module is: a
re-export of the `plexosdb-mcp` server object under that same name. The
install picker's instruction line and the `powermcp` package docstring carry
no decorative characters.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
powerio 0.11.2 ships a cp39-abi3 manylinux wheel serving Python 3.9 through
3.14, and the only other Rust dependency, surge-py, lives in an opt-in extra
CI never installs. Without the toolchain step a future wheel gap fails loudly
instead of building from an sdist.

The suite spawns roughly forty subprocesses per job with 60 second timeouts,
so stacked pushes on a busy runner turn a timeout into a failure. The
concurrency group cancels superseded pull request runs and leaves pushes to
main alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@samtalki samtalki changed the title Integrate PowerIO 0.11 modules and native Tellegen Studies Modernize every bridge for PowerIO 0.11.2 Sep 16, 2026
The tree is that of the eight commits ending at d1a8d91, which modernize
every bridge for powerio 0.11.2 and return the suite to green. The earlier
commits on this branch stay reachable through the second parent and
contribute nothing to the tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@qian-harvard qian-harvard left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at depth. The substance here is sound and unusually well tested — solver_case.py is a real improvement over the hand-rolled projection it replaces, and the typed-edit boundary is the right shape. I want this in. Three things first, plus a decision about this PR versus #71 that I think needs settling before either moves.

This PR, not #71

I checked whether #64 is still needed given #71, because on the surface #71 looks like a strict superset. It is:

$ git diff pr-71...pr-64 --stat
(empty)
$ comm -12 <(git diff --name-only main...pr-64|sort) <(git diff --name-only main...pr-71|sort) | wc -l
52          # all 52 of #64's files

So #71 contains every line of this PR. But it also bundles #67 (the Tellegen server) and #70 (the tool-result-shape change) — two drafts that have not been reviewed — and merging #71 today would land all four PRs in one click. That is not a trade I want to make for a 2,000-line dependency migration.

It also carries a test that fails. PLEXOSDB/plexosdb_mcp/main.py:183 returns tool_success(...) on #71, and PLEXOSDB/tests/test_tools.py:129 still asserts the old shape:

>>> tool_success(output_path="/tmp/out/system.json", model_name="Base", component_types={"Bus": 2})
{'status': 'success', 'output_path': '/tmp/out/system.json', 'model_name': 'Base', 'component_types': {'Bus': 2}}
>>> result["ok"]
KeyError: 'ok'

CI cannot see it: .github/workflows/test.yml:33 runs pytest tests/ only, so PLEXOSDB/tests/ is never collected. That belongs to #70 and I will raise it there — noting it here only because it is the reason I am not taking the superset.

So: this PR is the one to merge. #67, #69, #70 and #71 should then rebase onto the result, at which point their real diffs are small (#69 is 4 files, #71 is 23 of which 16 are one-token version bumps).

You will need a rebase, and that one is on me

I merged #72 an hour ago to cap powerio at <0.10. main had been red since 6 September — the <1 ceiling was resolving to 0.11.x against 0.9-era code, so pip install powermcp==0.3.0 was broken for everyone and CI was running 61 failed / 283 passed. That is now v0.3.1, 344 passed.

It collides with this PR in exactly the 15 files it touched — the 13 powerio pin sites plus powermcp/__init__.py and tests/test_registry.py. Every hunk is a one-line version string where both sides edited the same line, so the resolution is "take yours" throughout: your >=0.11.2,<0.12 supersedes my <0.10 cap, and your 0.4.0 supersedes 0.3.1. Sorry for the churn — the alternative was leaving a broken package on PyPI for however long this PR takes.

Please also re-run CI after rebasing. The last green run installed powerio 0.11.2, and 0.11.3 went up on 17 September, so the current pin now resolves to a version this branch has never been tested against. I checked the 0.11.2→0.11.3 delta myself and it is additive everywhere PowerMCP touches — allowed_roots() is byte-identical, the sandbox diff is comment-only — so I expect it to be clean. Worth the click anyway.

Findings

1. The multiconductor guard is unreachable for four of the five types it coverspowermcp/solver_case.py:478-483

if isinstance(value, _MULTICONDUCTOR):          # 5 types (:399-405)
    if not to_balanced or not isinstance(value, powerio.MulticonductorNetwork):
        raise ValueError("... or pass to_balanced=True")

_MULTICONDUCTOR is (MulticonductorNetwork, McAcPfInstance, McAcOpfInstance, McAcPfSolution, McAcOpfSolution). For the four that are not MulticonductorNetwork, the second clause is true regardless of to_balanced, so the raise fires unconditionally — and tells the caller to pass to_balanced=True, which is what they just did. A serialized McAcPfInstance is a dead end with advice that cannot work.

If lowering genuinely only applies to a network and not to a calculation instance, the message should say that instead of naming a flag. If instances should be lowerable, the guard is wrong.

2. powermcp doctor names its own startup directory as the servers' sandboxpowermcp/doctor.py:145-158

The policy description is right and the docstring is honest about it. The problem is {listed}: allowed_roots() runs in the doctor process, and powerio captures the startup directory at import, so the reported root is wherever powermcp doctor was invoked from. Servers launch as separate processes from the MCP client's working directory, which is usually somewhere else. An operator who runs the doctor from the repo, sees a plausible-looking path, and moves on has been told something that is not true of the thing they are about to run. Either name the directory the servers will actually start in, or drop the specific path and describe the rule.

3. The containment change needs a migration note, and wizard.py is where it should land

powermcp/sandbox.py:14-15 — powerio 0.11.2 flips unset roots from "nothing is constrained" to "confined to the server's startup directory". Every existing 0.3.x deployment that never set POWERIO_MCP_ALLOWED_ROOTS silently narrows on upgrade, and will surface as PathNotAllowed on paths that worked yesterday. The README prose covers it, but wizard.py's diff is 10 lines of pure punctuation — it still never prompts for roots, and the generated client entries carry no env block. At minimum this needs a line in the release notes; ideally the wizard asks.

Minor: the PR body says "GenX stops leaking the repository root onto sys.path", but GenX/server.py:22-24 still does sys.path.insert(0, _REPO_ROOT) exactly as main does — the change is three comment lines explaining why it stays. Worth fixing the description so a reviewer does not go looking for a change that is not there.

Explicitly dropping

I chased two concerns to the powerio source and they do not hold — flagging so nobody re-raises them:

  • The private _inner._type_name read is not at risk from 0.11.3. The 0.11.2→0.11.3 diff of python/powerio/__init__.py removes exactly one line and it is a docstring; _type_name is still declared at _powerio.pyi:879.
  • IR_VERSION being unused is real but the document["version"] KeyError it implies is unreachable: StoredModule declares version: u64 with deny_unknown_fields and no serde(default), so a document without it fails to deserialize before that subscript runs. Dead constant, not a bug.

Also worth saying: tests/test_publish_workflow.py plus the new sdist excludes for .venv* and .uv-build-cache are a genuine packaging fix — hatchling would otherwise ship a developer virtualenv to PyPI — and it is not mentioned in the description at all. It deserves a line.


Rebase, re-run CI, and answer 1. Then I will merge this and we can work through the stack on top of it.

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