Repository navigation
Releases
v0.52.0: a person arms with the vendor's ticket; the vendor's door answers on every product
What changed for an operator
The arm flow changed. A person no longer copies a write token into the served file: run proximo-core arm in a terminal of your own, type the password (and a second factor if the vendor asks), and the served file holds the vendor's ticket for two hours. proximo-core disarm is unchanged. A job with nobody at a keyboard keeps its token pair (PROXIMO_ARM_SOURCE) exactly as before. proximo mint pve now prints the pveum passwd line the ticket needs.
The release
A person arms with the vendor's ticket. A token is an identity (the vendor ships it with expire 0 and means a standing credential with an ACL); a ticket is a session (POST /access/ticket, the vendor's two hours, dead on its own). proximo-core arm now logs the operator in from a terminal of their own (a silent prompt for the password and, when the vendor asks, a second factor; sent once on the plane's own wire; an agent's shell passthrough has no terminal and is refused, nothing written) and serves the vendor's ticket at that plane's served path (--plane pve|pbs|pdm, each with its own served path, pair and login; --serve PATH names another file; --user names who logs in, default the served token's own user), 0600, with a ledger row naming the user and the hour it dies and never the ticket. The served file now holds a token or a ticket, read by one reader for the predicate and the wire: armed is this product's ticket, for this box, under two hours by the ticket's own signed issue time, failing closed on anything else; the wire sends a ticket as the vendor's cookie with its CSRF token on a mutation, and a dead ticket as the read-only token when one is configured. A job with nobody at a keyboard keeps the token pair (PROXIMO_ARM_SOURCE, served by configuration) unchanged, and disarm still refuses a read-only source byte-identical to it. Gone from the verb: copying a write token's bytes into the served file and reading GET /version with them first (the login is that check). A box that armed by copying a long-lived write token per session, with a reaper for sessions that ended armed, can retire both: nothing of ours needs reaping when the vendor's clock ends the arm (one such session on the author's box stayed armed four days behind two orphan tunnels). A served ticket goes only to the box and product that issued it: elsewhere, the read-only token rides or the call is refused before the wire. proximo mint pve prints pveum passwd <user> with the read token's recipe: a person arms as that user, a job needs no password. PBS and PDM tickets are the same shape under their own prefix; PMG keeps its password or credential file. A minor, on the digest lane's precedent of the vendor as the lock. Said in SECURITY (lab, 2026-11-05, under the credentials proximo mint installs): the vendor lets a user mint its own tokens, so a two-hour ticket can leave behind a standing token with the user's reach; the bound is the user's ACL, the vendor's, and Proximo adds nothing to it.
The vendor's door answers on every product. A plan's vendor_door (what the vendor's own permission check would answer the served credential, said beside Proximo's verdict, governing nothing) was PVE only. PBS and PDM are read now: their rule is proxmox-router's Permission (a check of path, privileges and partial, an or of checks and the self rule userParam, an and of checks), and what the credential holds is the caller's own GET /access/permissions, which those products answer as the whole tree of nodes the caller holds anything on, each privilege with its propagate flag: read once per credential; a listed node holds its privileges; a path the tree does not list is unknown, because the vendor's own walk (a NoAccess fence the tree cannot show, a node whose roles do not propagate) is not inferred from it; an empty tree is the vendor saying the caller holds nothing anywhere; a tree the evaluation cap cut (an Admin credential on a larger estate) is unknown whole. partial false needs every privilege, true any one. The self rule compares what the vendor compares, the whole auth id: MAY for a ticket's own user, NEVER for any token (the vendor's auth id for a token is the token id, which no user id equals), UNKNOWN when who is served could not be read. A rule of none is Permission::Superuser: MAY for the user root@pam itself (a root ticket; PMG's root credential), NEVER for every token, on every product. A rule slot the row's own path does not bind (POST /nodes/{node}/network on PBS and PDM carries {iface} in the body) is NEVER: the vendor fills it from the routed path only. PMG's rules are role lists; the served user's role is not read yet, so a PMG check answers in words naming the roles. Proved live on pbs-test under the credentials proximo mint pbs installs (a datastore create: never, Datastore.Allocate on /datastore; a comment on the granted datastore: may; the user's own token mint: may) and on pdm-test's minted pair (a notes write: never for the Auditor token, System.Modify on /system/notes; may for the Administrator token; the user's own token mint: may, by ticket). One whole-diff lens on 325b37e (0 BLOCKING, 4 MAJOR, 5 MINOR), each major a rule inferred past the vendor's words and folded by reading the vendor's source: the tree walk, the token's auth id, root on no rule, the body-bound slot. The door is still advice: a NEVER still sends and the vendor's 403 is the lock.
The vendor's digest is the write lock. Every config record Proxmox publishes carries a digest, and the write at that path accepts one: the vendor refuses a write whose digest is not the record's. A plan whose read shows a digest, on a call that publishes the parameter (197 served mutations across the four products: PVE 82, PBS 53, PDM 44, PMG 18), now carries it among its arguments, listed with the parameters given and shown whole to a model (a PBS or PDM digest is 64 characters, over the short-value cut), and the plan is judged on the consent id that covers those arguments, so an operator's grant binds to that version of the record; the call sends it, and a record that moved since the plan is the vendor's own refusal (400, "detected modified configuration file"), receipted as a wire error, nothing written. PBS and PDM hand a record's digest beside the record ({"data": record, "digest": hex}; read live on PBS: a datastore, a user, a token), where PVE puts it inside; the wire now keeps it with the record, so the lock holds on PBS and PDM as on PVE (PDM read live too: a user, a token, the realms); PMG publishes 18 such calls and is unverified. A record that is not a dict (PDM's /config/notes is a string) lends no lock. UNDO's delete-back (the reverse of a create whose record carries a digest, a PBS or PDM token) plans again at the act and sends the plan's own digest, so the grant an operator placed for its plan is the one the call spends. A call that sent no digest (consent inert, or a plan that could not read the record) writes under the digest its own read-back carried, when it took one (PROXIMO_UNDO=off takes none). The caller's own digest always wins. CANNOT: a record the plan could not show whole (over the cap, fenced, a collection, no GET twin) lends the plan no lock and current.why says so (the write still carries what its read-back took); a grant spent on a write the vendor then refused stays spent, and the operator approves the fresh plan. Before this, only UNDO's restore sent the digest; a consented write could land on a configuration that changed after the plan. A patch.
The vendor's permission rule rides every address. Each of the four api-viewers publishes, per call, the rule it checks before it answers (PVE: a headed check list; PMG: roles; PBS and PDM: a path, privileges and partial, or an or/and of them). proximo.json.gz now carries it verbatim as a permissions column on 1,729 of 1,799 vendor rows (the rest publish none; the shell lane's rows carry none). The build reads each rule against its product's grammar and refuses a form it does not know, with its words, so a new vendor shape stops the next refresh instead of riding in unread; the loader checks the column again, and a file nested past the parser's stack is refused like any unreadable file. It is the first step of the may/never map, below.
A PVE plan says what the vendor's own door would answer the token. A PVE plan now carries vendor_door: verdict (may, never, filtered, in words or unknown), why, and token, which token it answered for. It follows the plan's last word, needs: a plan refused for anything but the arm or consent (a held line, a lane not opted into, its arguments, its route, and on an armed box an untiered line, a taint wall or CONTAIN) carries none and asks nothing. On an unarmed box the arm is checked first and is the refusal named, so the door is asked even where a tier, a taint wall or CONTAIN would refuse the call too. An argument that does not read as its published type is named as the reason the door was not asked. The token is the one being served, as the arm read it at plan time, so on an unarmed box a write's answer is about the read-only token, and it says so. The answer is the address's rule evaluated in the vendor's own order (check_api2_permissions, exec_api2_perm_check) against what the token holds at each path the rule names, read from the vendor's own GET /access/permissions?path=, through the engine and receipted like any read. It governs nothing: allowed is still Proximo's verdict, a never is named and the call still goes when Proximo allows it, and the vendor stays the lock. It never says may where the vendor would refuse; where the map cannot be sure it says in words or unknown and why. On the lab, GET /pools/{poolid} for a pool the read token holds no Pool.Audit on answers never (Pool.Audit on /pool/<the pool>), which is the 403 the vendor send...
v0.51.0: AGPL-3.0-or-later, and a read stays under its cap
The license is AGPL-3.0-or-later from this release. A minor release for that reason: the rest are fixes. Every release through 0.50.1 was Apache-2.0 and stays Apache-2.0. The code Alessandro Del Prete (@alexdelprete) and RavenLiao contributed came in under Apache-2.0; those portions remain available under it as well, and its text ships beside the AGPL one (LICENSES/Apache-2.0.txt).
A read's answer stays under its cap. The answer cap (PROXIMO_RAW_MAX_BYTES, default 1,152 bytes and 750 estimated tokens) is what keeps a read small enough for a model's window. Every way an answer went past it is older than this release. The path a read was called on was clipped by characters, not by what it costs once sent, so an emoji path shipped over 2,000 bytes before a single row. A sight line, a wait= read's task words, the default node and the door's read-only note each joined the answer after it was sized, or were bounded alone and went over together. Now everything echoed beside the data shares one bound and is counted before the rows are cut, and the shrink loop can no longer grow the room it is shrinking. Measured through every published read with a path placeholder, path values of eight kinds of character at four lengths, twelve cap settings: 394,752 answered reads, none over.
The cap has a floor: its default. A cut answer still carries its header, its path, how to narrow it, its sight line and its notes, at most 1,136 bytes and 737 tokens. A cap set lower is raised to the default, and proximo doctor names the raise. Nothing changes for a setup that leaves the caps unset.
In a running door, a receipt costs the same on a long ledger as on a new one. Every call writes a receipt, and every receipt read the whole ledger twice. Measured on one machine, without a profiler: on 0.50.1 a read cost 3.2 ms on a new ledger and 486 ms at 37,632 rows; now 1.2 ms at 37,632 rows in a running door (the MCP server, the network doors). A new process, as each proximo-core call is, still reads the file once to learn the chain's mode. The chain is unchanged: the last hash is read from the end of the file and held to the old forward walk over every line shape a file can hold, and a chain that turns mixed is still called mixed.
Also: the shell lane keeps its exit code under a small token cap, and keeps more of a stream inside it. A cut answer's fields= advice names keys its rows really carry. pyasn1 0.6.5 for four advisories, reached only through the a2a extra.
Tested: 4,651 tests, and eleven adversarial review rounds on this release, each fix planted reversed and caught.
Full history for this release, commit by commit, is on the internal mirror; this public branch carries one squashed commit per release by design. Every change here is described in CHANGELOG.md.
Where to read more
- The license, and what stays Apache-2.0: README, License
- The answer cap, the floor, and the shell lane, with the variables: docs/SETUP.md
- Every operator variable in one place: packaging/proximo.env.example
- What each control honestly holds: SECURITY.md
- Verifying the image signature, the SBOM and the provenance yourself: VERIFY.md
- Full change detail: CHANGELOG.md
Install: uvx proximo-proxmox · pip install proximo-proxmox · ghcr.io/john-broadway/proximo:0.51.0
v0.50.1: the exit ceiling is the operator's to raise
The exit ceiling is the operator's to raise. A patch: one new variable, off until set. Nothing an existing setup sends changes.
What it is. Since 0.49.0 every answer a model is handed leaves the door under a byte ceiling: 1,152 for a read, 2,048 for a plan or a call, 3,072 for a diagnosis. One over its ceiling is replaced by a short fixed shape, not truncated. Those numbers are set for a small model on a small window. On a client whose window is not the constraint, the output is the whole point of the call, and alexdelprete measured what that cost on issue #114: a container store of 85 rows, docker ps, fifty log lines, proximo-core doctor, each gone whole, and PROXIMO_RAW_MAX_BYTES could not move them because it only trims below the exit.
PROXIMO_EXIT_MAX_BYTES raises all three ceilings to one number, in whole bytes from 3072 to 65536. Unset is the defaults, unchanged. It only raises: the floor is the largest default. The refusal ceiling (512) never moves. Set to anything else, the defaults stand, proximo doctor flags it (exit-ceiling-garbled), and the replaced answer says so when it has the room, never echoing the value. doctor reports the effective ceilings under DIAGNOSE. No argument a model sends changes it. The raw cap below the exit is a separate knob and still trims first: set both when a whole answer is the point of the call. Both at 16384 carry 8.7 KB of command output whole; he measured 65536 on both as byte-identical to the host for every output above.
With it, from his test of the branch. A cut exec stream now names PROXIMO_RAW_MAX_BYTES on its next line while neither raw cap is set and the answer has the room for it; the line is the first thing dropped when it does not, so it never costs the answer its dict. A cap the operator set, valid or not, names nothing, and doctor flags junk. proximo-core arm on a fresh state directory creates the served token's own directory (0700) instead of dying on it. The one-line description fits the MCP registry's 100 characters on every surface, and the copy gate holds it there.
Tested on his setup before it shipped. The branch was built from the branch Dockerfile and run beside his live 0.48.1 against the same container; eleven adversarial review rounds closed on it, ten of them breaking the round before and the last one clean. 4,386 tests.
Full history for this release, commit by commit, is on the internal mirror; this public branch carries one squashed commit per release by design. Every change here is described in CHANGELOG.md.
Where to read more
- The exit ceiling, the raw cap, and the shell wait, with the variables: docs/SETUP.md
- Every operator variable in one place: packaging/proximo.env.example
- The measurements and the test report: issue #114
- What each control honestly holds: SECURITY.md
- Verifying the image signature, the SBOM and the provenance yourself: VERIFY.md
- Full change detail: CHANGELOG.md
Install: uvx proximo-proxmox · pip install proximo-proxmox · ghcr.io/john-broadway/proximo:0.50.1
v0.50.0: the estate memory is back, with no model and no network
The estate memory is back, with no model and no network. A minor: one new capability, two new variables (PROXIMO_MEMORY, on by default, and PROXIMO_MEMORY_PATH), four new verbs on the core door. Nothing an existing setup sends changes; PROXIMO_MEMORY=0 creates nothing.
What Proximo has seen, answered from one file. Every governed read of a source the estate reads (cluster resources, storage, pools, backup jobs, ACLs, a node's network, a guest's config and pending) already fed memory.db, one SQLite file beside the ledger. proximo-core learn is the one deliberate pass, every read through the engine and receipted, and it says when it is incomplete and why. where 105 answers the kind, node, status, how old the answer is, and the published path that reads the thing live. uses local lists the guests with a disk or a NIC on a storage or bridge, by slot, and is incomplete while any guest's config is not in memory. changed 24h lists what was added, what is gone and whose status changed; gone means every whole read that had listed it has since read whole without it. A thing memory has not seen is "not in memory", never "does not exist", and an empty memory answers with no count at all. Derived, never proof: the ledger never reads it, and every memory answer is a core.recall row on it.
Owner-only, bounded, and taint survives storage. The file is created 0600 and opened read-write, never creating; a symlink, a hard link, another owner, a group or other bit, or an empty file memory did not create is refused, never quietly tightened. Names and tags are cut to 120 characters and an id longer than that is not kept; a read over 20,000 rows is not kept; a box holds at most 50,000 entities and 200,000 edges; every answer fits 4 KB and a path is never cut. A row from an untrusted address keeps the mark, and an answer built from it marks the session as the read did. Three adversarial rounds ran on the branch; each fix carries the probe that was red before it.
The one-line description says what Proximo is: the governed client for Proxmox, of which MCP is one door. On PyPI, the MCP registry, LobeHub, the Debian package and the LXC listing it read "the Proxmox MCP you can hand the keys", a frame that was never the product.
Also: docker/login-action 4.7.0 pinned by the SHA of its release (Dependabot #112); the curated public commit now ends with a Co-authored-by trailer for every outside author of the release range, and RavenLiao's #107 is merged on GitHub as his.
4,323 tests.
Full history for this release, commit by commit, is on the internal mirror; this public branch carries one squashed commit per release by design. Every change here is described in CHANGELOG.md.
Where to read more
- The estate memory, its verbs and its variables: docs/SETUP.md
- What each control honestly holds: SECURITY.md
- Verifying the image signature, the SBOM and the provenance yourself: VERIFY.md
- Full change detail: CHANGELOG.md
Install: uvx proximo-proxmox · pip install proximo-proxmox · ghcr.io/john-broadway/proximo:0.50.0
v0.49.1: shell calls across a cluster, and the operator's wait
Shell calls across a cluster, and the operator's wait. A patch: one new registry table and one new variable, both off until set; nothing an existing setup sends changes.
Route PVE shell rows by target and node. From RavenLiao, PR #107 (the node-routing half of #98). A named PVE target may map the node names in a path to OpenSSH destinations under [targets.<name>.nodes]. The five shell rows use that map for plans, execution and diagnosis. A missing mapping refuses before consent, rate slots, the safety snapshot or any read, and a named target never falls back to PROXIMO_SSH_TARGET. The far end must still answer as the path's node before pct runs. The default box is unchanged.
The shell wait is the operator's. PROXIMO_SHELL_TIMEOUT sets how long one shell call waits for its command, in whole seconds from 1 to 600; unset is the 60 it always was. Any other value refuses every shell call before anything dials, and proximo doctor reports the effective wait or flags it. No argument a model sends moves it. Issue #106.
Also: CI actions bumped, each pinned by the SHA of its release (#105), and a shell path's container id is ASCII digits only.
4,186 tests.
Full history for this release, commit by commit, is on the internal mirror; this public branch carries one squashed commit per release by design. Every change here is described in CHANGELOG.md.
Where to read more
- Shell lanes, node routing and the wait: docs/SETUP.md
- What each control honestly holds: SECURITY.md
- Verifying the image signature, the SBOM and the provenance yourself: VERIFY.md
- Full change detail: CHANGELOG.md
Install: uvx proximo-proxmox · pip install proximo-proxmox · ghcr.io/john-broadway/proximo:0.49.1
v0.49.0: a small model can drive the doors
A small model can drive the doors. A minor, inside door contract 3. The door, not the model, carries the token budget: an answer is data, a verdict and the next concrete path.
Every answer fits a hard ceiling. Every answer a model is handed, on MCP, REST and A2A, is measured as its final bytes at one exit. Ceilings: reads, finds and audit 1,152 bytes; plans, calls and an undo's plan 2,048; a diagnosis 3,072; a refusal 512. An answer over its ceiling is replaced by a short fixed shape: the verdict, needs, the consent_id, and what ran.
A model's plan does not echo its arguments. A mutation plan names every argument and shows a value of up to 48 characters; a longer one is [N chars sha256:<12 hex>]. The operator reads the whole proposal with proximo-core consent show <consent_id>, which refuses a ledger row that does not prove its own id. Which argument names are secrets is now an authored judgement in records/secrets.toml.
A rollback takes a safety snapshot first, on by default. A rollback discards everything since the snapshot, so the only net is another snapshot, taken through the datacenter manager or on the node. It uses storage; the plan says so before you approve. The operator turns it off with PROXIMO_UNDO=off, and no argument can skip it.
A plan can look past its target. Consult, the plan's second look, reads what a change would touch before it runs, through the same governed path, and only ever raises the tier.
Capabilities the 0.45 cut took come back, each on records/capabilities.toml with the tests that prove it: an empty Ceph flags call refused, rollback notes, a command risk hint, PROXIMO_NODE, PDM arrays, the reach grant on the ledger, the doctor receipt, the runbook prompts, the off-box anchor, find hearing typos, allowlists compared as grants, and token sight.
4,126 tests.
Full history for this release, commit by commit, is on the internal mirror; this public branch carries one squashed commit per release by design. Every change here is described in CHANGELOG.md.
Where to read more
- What each control honestly holds: SECURITY.md
- Verifying the image signature, the SBOM and the provenance yourself: VERIFY.md
- Full change detail: CHANGELOG.md
Install: uvx proximo-proxmox · pip install proximo-proxmox · ghcr.io/john-broadway/proximo:0.49.0
v0.48.1: pool tokens read their pool; what a clone needs
Pool tokens read their pool; what a clone needs. Issue #100 findings 2 and 3, measured again on a PVE 9.2.2 lab node. A patch: mint's printed recipe and SETUP; the engine, the data file and the door are unchanged.
Fixed: mint pve --write --scope /pool/<name> left both tokens unable to read the pool. In PVE an ACL on a deeper path replaces what a principal inherits there. The recipe's PVEVMAdmin on /pool/<name> for the user replaced the user's PVEAuditor from /, so the read token (its own rights intersected with the user's) kept only VM.Audit and VM.GuestAgent.Audit on the pool, the write token held no Pool.Audit, and GET /pools/<name> answered 403 to both. For every pool path in a PVE write scope (/pool or /pool/<name>) the recipe now also prints the grant of PVEAuditor on that path to the user and the write token, and says why; on the lab both reads came back and the only privileges added were the five audit ones. mint prints a recipe and calls nothing, so a token already minted for a pool is not fixed by upgrading: run the two PVEAuditor lines once, or re-mint. A guest scope's (/vms/<id>) grant commands and the other planes' recipes are unchanged.
New: what a clone outside the scope needs, said where you mint. A pool-scoped token, as minted, cannot clone a template kept outside the pool. Hunted on the lab from the vendor's own refusals, which name one missing privilege at a time, full and linked clones alike: VM.Clone on /vms/<template>, Datastore.AllocateSpace on /storage/<target> and SDN.Use on the bridge (/sdn/zones/localnetwork/<bridge> for a plain Linux bridge), each to the token and the user; the built-in roles PVETemplateUser, PVEDatastoreUser and PVESDNUser carry them, each with only its own audit privilege beside it. The PVE write recipe's grant step says so, and SETUP has a section, the commands, and two troubleshooting rows. Proximo does not grant those paths for you: which template, storage and bridge are yours to name. Reported with reproductions by kevwillow.
1,973 tests on Python 3.12 and 3.13; the mcp 1.x leg runs 1,971 and skips the two 2.x-only seams.
Full history for this release, commit by commit, is on the internal mirror; this public branch carries one squashed commit per release by design. Every change here is described in CHANGELOG.md.
Where to read more
- A pool scope, and cloning into it (the commands): docs/SETUP.md
- What each control honestly holds: SECURITY.md
- Verifying the image signature, the SBOM and the provenance yourself: VERIFY.md
- Full change detail: CHANGELOG.md
Install: uvx proximo-proxmox · pip install proximo-proxmox · ghcr.io/john-broadway/proximo:0.48.1
v0.48.0: a restore says what identity it keeps
A restore says what identity it keeps. A minor: a new authored record and a new plan key (door contract 2). No verdict moves, and nothing Proximo sends the vendor changes.
Said, not defaulted: what #82 asked for, which the 0.45 cut lost. The call is unchanged: a restore without unique still gives the copy the backup's identity, as Proxmox does; the plan now says so before it runs. Issue #100, from the reporter of #82: 0.44.2's fix for #82 (a restored copy wore the original's MAC and SMBIOS UUID) lived in pve_restore, and the 0.45.0 cut removed it with the other wrappers without naming it. Measured again on a PVE 9.2.2 lab node through the shell door: a QEMU restore to a new VMID without unique came back with the source's MAC and SMBIOS UUID, and its plan said nothing about either; with unique=1 both were new.
New: plan notes. records/notes.toml holds consequences the tier cannot say, keyed on the address like every record: a condition read from the call's own arguments (given, not_given; a boolean read through the same words coerce_args sends the vendor, a string given unless blank), what the plan says, and why. Only a definite answer silences a note: a value that cannot be told before the wire (a boolean of 2, a list, a word nobody reads as a boolean) shows it. A plan whose arguments meet a line carries notes: [{says, why}], and the receipt's executing row carries the same sentences under notes. A note never reads the target and never changes a verdict: it shows the same armed, unarmed, with no credential or with the target's read refused, and the tier still decides. Two lines ship: a QEMU restore (archive) without unique says it keeps the backup's MAC and SMBIOS UUID, a container restore (restore) without unique says it keeps the backup's MAC; each says it is right for a rebuild in place and that a copy wants unique=1. Proximo does not send unique for you: 0.44.2 did, by parsing the archive's name for its source VMID, and the core does not change what the vendor's call does. The build refuses a note on an address that is not a served mutation, a name the address does not publish or names on both sides, an empty condition, or a missing sentence; and a name twice, a name the path binds, a sentence with space around it, or two notes with one condition; the loader refuses a file carrying a note the build would not write.
The door is door contract 2. The plan answer gains notes (a list of says and why), present only when a note holds; docs/door-contract.json samples a restore so the key is part of the contract. Nothing else in the door moved.
Proved live. On a PVE 9.2.2 lab node through the shell door: the restore without unique kept the source's MAC and SMBIOS UUID and its plan and receipt carried the note; unique=1 gave new ones and carried none. The container line rests on the vendor's sentence; the lab had no container template to restore. Two adversarial lenses went over the branch: the first found the plan reading unique=n as given while the vendor was sent false (the note silent on exactly the call it exists for); one boolean reader now serves both, and a test sends every spelling through the plan and the receipt and holds them to one answer.
1,964 tests on Python 3.12 and 3.13; the mcp 1.x leg runs 1,962 and skips the two 2.x-only seams.
Full history for this release, commit by commit, is on the internal mirror; this public branch carries one squashed commit per release by design. Every change here is described in CHANGELOG.md.
Where to read more
- The notes and their sentences, and how a record line is judged: records/notes.toml and records/README.md
- The door contract a client builds against (now 2): docs/door-contract.json
- What the receipt's rows carry,
notesamong them: docs/ledger-format.md - What each control honestly holds: SECURITY.md
- Verifying the image signature, the SBOM and the provenance yourself: VERIFY.md
- Full change detail: CHANGELOG.md
Install: uvx proximo-proxmox · pip install proximo-proxmox · ghcr.io/john-broadway/proximo:0.48.0
v0.47.0: self-owned grants refused; a stdlib ledger checker
Self-owned grants refused; a stdlib ledger checker. A minor: it refuses consent grants that worked before, adds an opt-in for one-user boxes, and adds a checker and a written ledger format.
Changed (refuses what worked before). SECURITY.md's hardened recipe put consent grants in /etc/proximo, mode 1770 root:proximo: the agent's group could create files there, and the gate never asked who placed a grant, so the agent could write its own and it was accepted. A grant owned by the agent's own user is now refused (consent: "self"). On an install where the human and the agent are one user, root included, consent refuses every mutation it guards until the agent runs as its own account. The recipe now gives grants their own directory, owned by the operator's account, that the agent can read and not create in; packaging/proximo.env.example shows it.
New: PROXIMO_CONSENT_SAME_USER=advisory. For a one-user box where consent is the agent stopping to ask, not a lock: same-user grants are accepted, still spent once and still bound by the TTL, and never called granted. The plan shows consent: "advisory" and says so in its reason, every such spend receipt carries "advisory": true, and DIAGNOSE flags the box. It trusts the agent not to write its own grant, which the default refuses to trust. Off unless exactly advisory; any other value refuses mutations; a blank value is unset.
Fixed: the spend. It was os.remove, so a root-placed grant in that sticky directory could never be spent: the agent's grants worked and the operator's did not. A grant now spends once by a marker beside the ledger (.proximo-consent-spent/), with a core.consent_spent row as the evidence. The grant is judged and spent on one lstat, the marker is keyed on that file (device, inode, mtime; re-writing the grant is a new approval, renaming it onto another plan is not), and the core removes the grant only if the path still holds that file. If the spend's receipt cannot be written, nothing reaches the wire and the grant is not used up. proximo_plan shows a used grant as consent: "spent".
New: the ledger format, written down, and a checker that needs no Proximo. docs/ledger-format.md (format ledger: 1) and scripts/verify_ledger.py, one file of standard-library Python: whoever is handed a log and its key can check it. Its tests hold it to the core on every planted break, file by file.
Fixed: Proximo's own ledger check. It read the log in text mode, so a lone \r the writer never writes was read as a row break, and the checker and audit_verify could give opposite verdicts on one file; every check that gives a verdict now reads the rows as the writer joined them. A row with a 5,000-digit number, deep nesting, a lone surrogate or bytes that are not UTF-8 crashed audit_verify; each is now a broken row at its line.
Tiers: the re-cut. VM start, shutdown, resume, snapshot create and snapshot description are restorable on their own lines (they shared the destructive qemu family with destroy); VM and container reboot and VM suspend are disruptive; container resume, snapshot create and snapshot description are restorable. vzdump, PBS sync jobs, PBS realm sync and pve:/access/users are destructive; the PBS notification test and S3 check are disruptive. Severity still gates nothing on its own: a tier is what a plan and its receipt say.
Security. opentelemetry-api, which mcp pulls in and uses to extract trace context, moves from 1.44.0 to 1.45.1, past CVE-2026-114603: a crafted tracestate header took quadratic time to parse. The lock and the hash-pinned requirements carry the fix; CI's pip-audit refused the release until they did.
1,858 tests on Python 3.12 and 3.13; the mcp 1.x leg runs 1,856 and skips the two 2.x-only seams.
Full history for this release, commit by commit, is on the internal mirror; this public branch carries one squashed commit per release by design. Every change here is described in CHANGELOG.md.
Where to read more
- Consent, the owner check and the same-user opt-in, and what each control honestly holds: SECURITY.md
- The ledger format, and checking a log without Proximo: docs/ledger-format.md and scripts/verify_ledger.py
- What each threat is met by, and the residual risks said plainly: docs/THREAT_MODEL.md
- The tiers and their sentences: records/README.md
- Verifying the image signature, the SBOM and the provenance yourself: VERIFY.md
- Full change detail: CHANGELOG.md
Install: uvx proximo-proxmox · pip install proximo-proxmox · ghcr.io/john-broadway/proximo:0.47.0
v0.46.2: six columns derived; two dead example keys removed
Six columns derived; two dead example keys removed. A patch: a stricter loader, a corrected example file, and tests. No new capability; door contract 1 is unchanged and proximo.json.gz is byte-for-byte 0.46.0's.
The loader derives six columns from the data file's own rows. 0.46.1 read access as written and checked that the columns twin, state, expect, task status, lane and creates named rows that existed. A forged proximo.json.gz with a POST marked read was served as a read, past the arm, tier and consent gates; a lane on an ungated row, a state beside a served twin, a create on a read, and the other shapes the build never writes were served too. The loader now computes access, twin, state, expect, task status and lane from the file's rows the way the build does (access from the method, the rest from the file's own reads and twins) and refuses the file whole on the first column that differs, naming the column and both values; a creates is held to the build's eligibility rule (an ungated POST with served GET and DELETE twins at its path), since the build takes which rows create from an authored record. A snapshot parameter that is not a table is refused with a sentence instead of a traceback. The loader is no stricter than the build: every shape the build writes loads.
The example registry teaches only the keys the core reads (#98). packaging/targets.example.toml showed ssh_target on one target and node on three. The loader the 0.45.0 cut removed read them; the core never has, and the registry ignores a key it does not know, so an operator who set them got nothing. 0.45.0 through 0.46.1 shipped the example that way. The four lines are gone, the header says the ssh lanes dial only the env's PROXIMO_SSH_TARGET and that the node a call means is in its path, and a test drives the real loader over the shipped example and fails on any key whose value can change without the loader noticing.
1,780 tests on Python 3.12 and 3.13; the mcp 1.x leg runs 1,778 and skips the two 2.x-only seams.
Full history for this release, commit by commit, is on the internal mirror; this public branch carries one squashed commit per release by design. Every change here is described in CHANGELOG.md.
Where to read more
- The one JSON and its records: records/README.md
- The target registry's shape: packaging/targets.example.toml
- The door contract, and what a client may build against: docs/door-contract.json
- The trust model, and the honest scope notes saying what does not hold: SECURITY.md
- Verifying the image signature, the SBOM and the provenance yourself: VERIFY.md
- Full change detail: CHANGELOG.md
Install: uvx proximo-proxmox · pip install proximo-proxmox · ghcr.io/john-broadway/proximo:0.46.2