PyxGrant / Changelog
Changelog
What changed in PyxGrant, newest first. Recent releases close gaps found in our own audit of the enforcement code, each with a regression test.
Windows can wrap an MCP server again
- Launching a child over stdio used ExtraFiles, which Windows rejects, so a wrapped server never started. The child starts again. On Windows the scope token goes in the environment, which is weaker than the Unix pipe: a process that can read that child's environment can read the token.
pyxgrant incidentsalso marks five catalog rows partial where the product had overstated coverage, including SKILL.md-only pinning and Landlock only throughsandbox contain.
A restricted tool's streamed result waits for the verdict
- A held-class or irreversible tool result on SSE used to start leaving before the decision finished. Those bytes are now buffered. A deny releases none of them. Bytes that already left on another stream stay sent.
If the policy engine is down, the hook says so and refuses
- When the coding-agent hook cannot reach Open Policy Agent, the reason now includes
PEP_UNREACHABLEand the call is denied. Setopa.on_errortoholdorallowif you want a different answer; the signal is still written.
A document a coding agent just read is scanned for active content
- A PDF, Office file, or similar that comes back through Claude Code or Cursor never crossed the MCP proxy, so the gateway never saw it. The PostToolUse hook now extracts it, refuses active content such as PDF JavaScript or an Office macro, and scans the text for injection and secrets.
A payment reverse is not closed until the provider confirms
pyxgrant pay reverseruns the provider void, then reads the charge back. Exit 0 only when the provider says the money is gone; exit 3 leaves the ticket open. The core binary ships the command and no live Stripe or Adyen connector. Until an adapter registers one, reverse reports that rather than marking the charge void.
A model call spends the same agency budget as a tool call
- Each forwarded completion charges one step on the shared grant counter, so a fleet cannot escape a spent wallet by moving to the model plane. A spent counter returns 429
agency-exhausted. Under the hardened profile, a model with no configured price is refused with 403model-unpriced.
Fleet containment needs two people
pyxgrant contain actrecords pause and isolate with a 15-minute undo. Panic, wipe_memory, maintenance_bypass, and kill_fleet need a second, different operator on-confirm. Panic and kill_fleet kill the target grant on the grant store, the revocation file, and the signed death stream.
A raw Modbus frame can drive the OT decision
pyxgrant ot check -modbusdecodes a hex Modbus TCP frame and picks the operation from the function code. An unreadable frame is treated as an act, not a read. PyxGrant still decides; your OT gateway still speaks the wire.
Discover now lists ungoverned local model ports
pyxgrant discoveralready flagged MCP servers not routed through PyxGrant. It now also lists Ollama, LM Studio, and vLLM-style listeners on the machine with no gateway in front. Those count toward the same unhooked exit code.
A prove pack cannot sit in the same tree as its anchor
pyxgrant prove -anchorused to refuse only when the anchor path was exactly the output directory. An anchor inside the pack, a pack inside the anchor, a symlink, or a.tarlanding in the same tree still passed. Those cases are refused now: one holder must not control both the pack and the proof.
Hardened now requires a host-plane watcher
- A hardened gateway that never configured a host heartbeat could not tell that the host plane had gone dark. Production-check now fails with
host-unwatchedunlesssecurity.host_heartbeat_fileis set.
The console stays on loopback unless you opt out
- The admin console refuses a non-loopback bind on every profile, unless you pass
-allow-nonloopback. TLS on the console encrypts the wire; it does not decide who can reach the UI.
Hardened seals need an external key custodian
- A local key file or a local anchor directory no longer counts as an independent witness. Hardened startup requires
audit.signing_key_kms. The read-only console is not the sealer, so it no longer fails that check.
The agentic map uses OWASP's published ASI names
- The old AAI01–AAI10 labels were ours. The map now uses ASI01–ASI10. Inter-agent communication and cascading failures are marked partial, not covered.
Recovery stays open until a read-back proves it
- A rollback that cannot confirm the restored state no longer closes as resolved. A snapshot-undo action is refused if the pre-call snapshot cannot be taken.
A failed exactly-once check now refuses, and an approved agent hop is charged
- Irreversible calls are claimed once so they can't run twice. If that claim couldn't be stored or locked, the call used to go through. Now it is refused.
- An agent-to-agent hop that a person approved used to skip the shared grant budget. Approved hops are now charged like any other, and a storage error refuses the hop.
Revoking a grant is tied to the decision that caused it
- When a denied call ends a grant, the revocation receipt, the revocation file, and the signed death event now all carry the deciding record's ID, so an auditor can link the three to one decision.
A model can't slip content past an output check by streaming
- When an output check is on, PyxGrant asks the model for a complete response it can inspect. A model that streamed anyway used to get its output through unchecked. Now that response is refused with a 502, and the refusal is recorded.
Marking a tool irreversible now holds it and ends its grant on refusal
- A tool marked as irreversible now waits for a person on its own, without a separate approval setting. Observe mode still records the hold without waiting.
- Refusing an irreversible tool now ends the grant behind it, so the next attempt meets a dead grant.
The SQL guard covers writes, and the container image builds again
- A tool restricted to reading certain columns could still run DELETE, UPDATE, DROP, or INSERT, because only statements containing SELECT were checked. Every SQL field is now checked, and write statements are refused.
- The container image pointed at the binary's old name and would not build. It builds pyxgrant again.
A clean static-analysis run
- The Go security scanner now runs clean on PyxGrant's own code. Findings were fixed, such as a header timeout and tighter file permissions, or marked at the line with the reason they are safe.